From 42e924952d42289f0e308679d25954d0b2871e73 Mon Sep 17 00:00:00 2001 From: YuCheng Hu Date: Tue, 30 Nov 2021 13:30:11 -0500 Subject: [PATCH] USRE-88 Push new file for example --- rets-io-client/pom.xml | 266 +++++++ .../client/BrokerCodeRequredException.java | 30 + .../ossez/usreio/client/CapabilityUrls.java | 161 ++++ .../usreio/client/ChangePasswordRequest.java | 40 + .../usreio/client/ChangePasswordResponse.java | 36 + .../CollectionOfCollectionsIterator.java | 40 + .../usreio/client/CommonsHttpClient.java | 180 +++++ .../client/CommonsHttpClientResponse.java | 110 +++ .../ossez/usreio/client/CompactRowPolicy.java | 56 ++ .../usreio/client/GenericHttpRequest.java | 36 + .../usreio/client/GetMetadataRequest.java | 73 ++ .../usreio/client/GetMetadataResponse.java | 80 ++ .../usreio/client/GetObjectIterator.java | 12 + .../ossez/usreio/client/GetObjectRequest.java | 101 +++ .../usreio/client/GetObjectResponse.java | 188 +++++ .../client/GetObjectResponseIterator.java | 140 ++++ .../client/InvalidArgumentException.java | 7 + .../client/InvalidHttpStatusException.java | 12 + .../client/InvalidReplyCodeException.java | 44 ++ .../client/InvalidReplyCodeHandler.java | 17 + .../ossez/usreio/client/KeyValueResponse.java | 140 ++++ .../com/ossez/usreio/client/LoginRequest.java | 23 + .../ossez/usreio/client/LoginResponse.java | 200 +++++ .../ossez/usreio/client/LogoutRequest.java | 9 + .../ossez/usreio/client/LogoutResponse.java | 41 + .../usreio/client/MetaCollectorAdapter.java | 61 ++ .../usreio/client/MetaCollectorImpl.java | 22 + .../usreio/client/NetworkEventMonitor.java | 23 + .../client/NullNetworkEventMonitor.java | 13 + .../com/ossez/usreio/client/ReplyCode.java | 100 +++ .../ossez/usreio/client/ReplyCodeHandler.java | 23 + .../ossez/usreio/client/RetsException.java | 22 + .../ossez/usreio/client/RetsHttpClient.java | 36 + .../ossez/usreio/client/RetsHttpRequest.java | 81 ++ .../ossez/usreio/client/RetsHttpResponse.java | 26 + .../com/ossez/usreio/client/RetsSession.java | 403 ++++++++++ .../ossez/usreio/client/RetsTransport.java | 334 +++++++++ .../com/ossez/usreio/client/RetsUtil.java | 48 ++ .../com/ossez/usreio/client/RetsVersion.java | 106 +++ .../ossez/usreio/client/SearchRequest.java | 122 +++ .../com/ossez/usreio/client/SearchResult.java | 23 + .../usreio/client/SearchResultCollector.java | 18 + .../usreio/client/SearchResultHandler.java | 280 +++++++ .../ossez/usreio/client/SearchResultImpl.java | 87 +++ .../ossez/usreio/client/SearchResultInfo.java | 25 + .../usreio/client/SearchResultProcessor.java | 13 + .../ossez/usreio/client/SearchResultSet.java | 15 + .../usreio/client/SingleObjectResponse.java | 53 ++ .../usreio/client/SinglePartInputStream.java | 64 ++ .../StreamingSearchResultProcessor.java | 324 ++++++++ .../client/VersionInsensitiveRequest.java | 15 + .../client/retsapi/InputStreamDataSource.java | 126 ++++ .../client/retsapi/RETSActionTransaction.java | 35 + .../retsapi/RETSBasicResponseParser.java | 128 ++++ .../RETSChangePasswordTransaction.java | 176 +++++ .../usreio/client/retsapi/RETSConnection.java | 635 ++++++++++++++++ .../retsapi/RETSGetMetadataTransaction.java | 51 ++ .../retsapi/RETSGetObjectTransaction.java | 339 +++++++++ .../client/retsapi/RETSLoginTransaction.java | 184 +++++ .../client/retsapi/RETSLogoutTransaction.java | 32 + .../retsapi/RETSSearchAgentTransaction.java | 32 + .../retsapi/RETSSearchOfficeTransaction.java | 27 + .../RETSSearchPropertyBatchTransaction.java | 26 + .../RETSSearchPropertyTransaction.java | 20 + .../client/retsapi/RETSSearchTransaction.java | 321 ++++++++ .../RETSServerInformationTransaction.java | 50 ++ .../client/retsapi/RETSTransaction.java | 277 +++++++ .../client/retsapi/RETSUpdateTransaction.java | 156 ++++ .../tests/common/metadata/AttrType.java | 16 + .../common/metadata/JDomCompactBuilder.java | 702 ++++++++++++++++++ .../common/metadata/JDomStandardBuilder.java | 628 ++++++++++++++++ .../tests/common/metadata/MetaCollector.java | 20 + .../tests/common/metadata/MetaObject.java | 366 +++++++++ .../common/metadata/MetaParseException.java | 26 + .../tests/common/metadata/Metadata.java | 154 ++++ .../common/metadata/MetadataBuilder.java | 203 +++++ .../common/metadata/MetadataElement.java | 31 + .../common/metadata/MetadataException.java | 27 + .../tests/common/metadata/MetadataType.java | 30 + .../metadata/attrib/AttrAbstractText.java | 49 ++ .../common/metadata/attrib/AttrAlphanum.java | 31 + .../common/metadata/attrib/AttrBoolean.java | 54 ++ .../common/metadata/attrib/AttrDate.java | 71 ++ .../common/metadata/attrib/AttrEnum.java | 31 + .../metadata/attrib/AttrGenericText.java | 31 + .../common/metadata/attrib/AttrNumeric.java | 31 + .../metadata/attrib/AttrNumericPositive.java | 36 + .../common/metadata/attrib/AttrPlaintext.java | 28 + .../common/metadata/attrib/AttrText.java | 28 + .../common/metadata/attrib/AttrVersion.java | 66 ++ .../tests/common/metadata/types/MClass.java | 126 ++++ .../common/metadata/types/MEditMask.java | 50 ++ .../common/metadata/types/MForeignKey.java | 87 +++ .../tests/common/metadata/types/MLookup.java | 86 +++ .../common/metadata/types/MLookupType.java | 56 ++ .../tests/common/metadata/types/MObject.java | 79 ++ .../common/metadata/types/MResource.java | 270 +++++++ .../common/metadata/types/MSearchHelp.java | 50 ++ .../tests/common/metadata/types/MSystem.java | 89 +++ .../tests/common/metadata/types/MTable.java | 242 ++++++ .../tests/common/metadata/types/MUpdate.java | 91 +++ .../common/metadata/types/MUpdateHelp.java | 50 ++ .../common/metadata/types/MUpdateType.java | 103 +++ .../metadata/types/MValidationExpression.java | 61 ++ .../metadata/types/MValidationExternal.java | 76 ++ .../types/MValidationExternalType.java | 57 ++ .../metadata/types/MValidationLookup.java | 76 ++ .../metadata/types/MValidationLookupType.java | 56 ++ .../com/ossez/usreio/util/SessionUtils.java | 62 ++ rets-io-client/src/main/resources/build.xml | 105 +++ .../src/main/resources/default.properties | 1 + rets-io-client/src/main/resources/dummy.dtd | 3 + .../src/main/resources/log4j.properties | 38 + rets-io-client/src/main/resources/log4j2.xml | 97 +++ rets-io-client/src/main/resources/logback.xml | 21 + .../src/main/resources/metadata_tables.xml | 41 + .../src/main/resources/templates.xml | 179 +++++ .../ossez/usreio/tests/client/AllTests.java | 31 + .../usreio/tests/client/ConnectionTest.java | 52 ++ .../tests/client/GetMetadataRequestTest.java | 58 ++ .../tests/client/GetMetadataResponseTest.java | 156 ++++ .../client/GetObjectResponseIteratorTest.java | 194 +++++ .../usreio/tests/client/IOFailReader.java | 35 + .../usreio/tests/client/LoginRequestTest.java | 24 + .../tests/client/LoginResponseTest.java | 133 ++++ .../tests/client/LogoutResponseTest.java | 57 ++ .../tests/client/RetsGetObjectExample.java | 122 +++ .../tests/client/RetsGetObjectURLExample.java | 94 +++ .../usreio/tests/client/RetsMetadataTest.java | 96 +++ .../tests/client/RetsSearchExample.java | 84 +++ .../usreio/tests/client/RetsSessionTest.java | 69 ++ .../usreio/tests/client/RetsTestCase.java | 87 +++ .../usreio/tests/client/RetsVersionTest.java | 31 + .../tests/client/SearchResultHandlerTest.java | 153 ++++ .../tests/client/SearchResultImplTest.java | 58 ++ .../client/SingleObjectResponseTest.java | 37 + .../StreamingSearchResultProcessorTest.java | 200 +++++ .../client/TestInvalidReplyCodeHandler.java | 20 + .../tests/client/objects-missing.multipart | Bin 0 -> 105875 bytes .../tests/common/metadata/MetaObjectTest.java | 44 ++ .../common/metadata/MetadataTestCase.java | 14 + .../tests/common/metadata/TestMetaObject.java | 48 ++ .../metadata/attrib/AttrAbstractTextTest.java | 38 + .../metadata/attrib/AttrAlphanumTest.java | 45 ++ .../metadata/attrib/AttrBooleanTest.java | 46 ++ .../common/metadata/attrib/AttrDateTest.java | 27 + .../common/metadata/attrib/AttrEnumTest.java | 17 + .../metadata/attrib/AttrGenericTextTest.java | 17 + .../metadata/attrib/AttrNumericTest.java | 21 + .../metadata/attrib/AttrPlaintextTest.java | 27 + .../common/metadata/attrib/AttrTextTest.java | 23 + .../common/metadata/attrib/AttrTypeTest.java | 28 + .../metadata/attrib/AttrVersionTest.java | 32 + .../util/CaseInsensitiveTreeMapTest.java | 54 ++ .../src/test/resources/2237858_0.jpg | Bin 0 -> 76861 bytes .../getMetadataResponse_lookupZero.xml | 18 + .../getMetadataResponse_noRecords.xml | 1 + .../resources/getMetadataResponse_system.xml | 6 + .../getMetadataResponse_updateType.xml | 7 + rets-io-client/src/test/resources/log4j2.xml | 97 +++ rets-io-client/src/test/resources/logback.xml | 21 + .../src/test/resources/login_lower_case.xml | 17 + .../resources/login_response_valid_1.0.xml | 16 + .../resources/login_response_valid_1.5.xml | 18 + .../resources/login_response_valid_1.7.xml | 17 + .../resources/login_response_valid_1.8.xml | 17 + .../resources/login_response_valid_1.9.xml | 17 + .../src/test/resources/login_valid10.xml | 16 + .../src/test/resources/login_valid15.xml | 17 + .../src/test/resources/logout_lower_case.xml | 6 + .../src/test/resources/logout_no_equals.xml | 4 + .../src/test/resources/logout_valid10.xml | 5 + .../src/test/resources/logout_valid15.xml | 6 + .../test/resources/objects-missing.multipart | Bin 0 -> 105875 bytes .../src/test/resources/rets.properties | 4 + rets-io-common/README.md | 2 + rets-io-common/pom.xml | 123 +++ .../common/util/AttributeExtracter.java | 62 ++ .../usreio/common/util/Base64Encoder.java | 230 ++++++ .../common/util/Base64FormatException.java | 19 + .../util/CaseInsensitiveComparator.java | 12 + .../common/util/CaseInsensitiveTreeMap.java | 17 + .../usreio/common/util/CompactFormatData.java | 302 ++++++++ .../usreio/common/util/CompactParser.java | 208 ++++++ .../ossez/usreio/common/util/DesCrypter.java | 92 +++ .../ossez/usreio/common/util/DigestUtil.java | 150 ++++ .../ossez/usreio/common/util/HexUtils.java | 88 +++ .../com/ossez/usreio/common/util/MD5Util.java | 68 ++ .../usreio/common/util/PropertiesLocator.java | 34 + .../util/PropertiesNotFoundException.java | 17 + .../common/util/RETSCompactHandler.java | 186 +++++ .../usreio/common/util/RETSConfigurator.java | 24 + .../usreio/common/util/RETSReplyCodes.java | 46 ++ .../common/util/RETSRequestResponse.java | 63 ++ .../ossez/usreio/common/util/Resource.java | 214 ++++++ .../usreio/common/util/ResourceLocator.java | 58 ++ .../ossez/usreio/common/util/XMLUtils.java | 249 +++++++ .../src/main/resources/log4j.properties | 38 + 198 files changed, 16789 insertions(+) create mode 100644 rets-io-client/pom.xml create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/BrokerCodeRequredException.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/CapabilityUrls.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/ChangePasswordRequest.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/ChangePasswordResponse.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/CollectionOfCollectionsIterator.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/CommonsHttpClient.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/CommonsHttpClientResponse.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/CompactRowPolicy.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/GenericHttpRequest.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/GetMetadataRequest.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/GetMetadataResponse.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectIterator.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectRequest.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectResponse.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectResponseIterator.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/InvalidArgumentException.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/InvalidHttpStatusException.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/InvalidReplyCodeException.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/InvalidReplyCodeHandler.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/KeyValueResponse.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/LoginRequest.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/LoginResponse.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/LogoutRequest.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/LogoutResponse.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/MetaCollectorAdapter.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/MetaCollectorImpl.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/NetworkEventMonitor.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/NullNetworkEventMonitor.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/ReplyCode.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/ReplyCodeHandler.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/RetsException.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/RetsHttpClient.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/RetsHttpRequest.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/RetsHttpResponse.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/RetsSession.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/RetsTransport.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/RetsUtil.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/RetsVersion.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/SearchRequest.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/SearchResult.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultCollector.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultHandler.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultImpl.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultInfo.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultProcessor.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultSet.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/SingleObjectResponse.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/SinglePartInputStream.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/StreamingSearchResultProcessor.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/VersionInsensitiveRequest.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/InputStreamDataSource.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSActionTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSBasicResponseParser.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSChangePasswordTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSConnection.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSGetMetadataTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSGetObjectTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSLoginTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSLogoutTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchAgentTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchOfficeTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchPropertyBatchTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchPropertyTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSServerInformationTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSUpdateTransaction.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/AttrType.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/JDomCompactBuilder.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/JDomStandardBuilder.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetaCollector.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetaObject.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetaParseException.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/Metadata.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataBuilder.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataElement.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataException.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataType.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrAbstractText.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrAlphanum.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrBoolean.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrDate.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrEnum.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrGenericText.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrNumeric.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrNumericPositive.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrPlaintext.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrText.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrVersion.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MClass.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MEditMask.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MForeignKey.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MLookup.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MLookupType.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MObject.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MResource.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MSearchHelp.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MSystem.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MTable.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MUpdate.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MUpdateHelp.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MUpdateType.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationExpression.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationExternal.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationExternalType.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationLookup.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationLookupType.java create mode 100644 rets-io-client/src/main/java/com/ossez/usreio/util/SessionUtils.java create mode 100644 rets-io-client/src/main/resources/build.xml create mode 100644 rets-io-client/src/main/resources/default.properties create mode 100644 rets-io-client/src/main/resources/dummy.dtd create mode 100644 rets-io-client/src/main/resources/log4j.properties create mode 100644 rets-io-client/src/main/resources/log4j2.xml create mode 100644 rets-io-client/src/main/resources/logback.xml create mode 100644 rets-io-client/src/main/resources/metadata_tables.xml create mode 100644 rets-io-client/src/main/resources/templates.xml create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/AllTests.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/ConnectionTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/GetMetadataRequestTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/GetMetadataResponseTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/GetObjectResponseIteratorTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/IOFailReader.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/LoginRequestTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/LoginResponseTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/LogoutResponseTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsGetObjectExample.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsGetObjectURLExample.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsMetadataTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsSearchExample.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsSessionTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsTestCase.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsVersionTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/SearchResultHandlerTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/SearchResultImplTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/SingleObjectResponseTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/StreamingSearchResultProcessorTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/TestInvalidReplyCodeHandler.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/client/objects-missing.multipart create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/MetaObjectTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/MetadataTestCase.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/TestMetaObject.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/attrib/AttrAbstractTextTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/attrib/AttrAlphanumTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/attrib/AttrBooleanTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/attrib/AttrDateTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/attrib/AttrEnumTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/attrib/AttrGenericTextTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/attrib/AttrNumericTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/attrib/AttrPlaintextTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/attrib/AttrTextTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/attrib/AttrTypeTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/metadata/attrib/AttrVersionTest.java create mode 100644 rets-io-client/src/test/java/com/ossez/usreio/tests/common/util/CaseInsensitiveTreeMapTest.java create mode 100644 rets-io-client/src/test/resources/2237858_0.jpg create mode 100644 rets-io-client/src/test/resources/getMetadataResponse_lookupZero.xml create mode 100644 rets-io-client/src/test/resources/getMetadataResponse_noRecords.xml create mode 100644 rets-io-client/src/test/resources/getMetadataResponse_system.xml create mode 100644 rets-io-client/src/test/resources/getMetadataResponse_updateType.xml create mode 100644 rets-io-client/src/test/resources/log4j2.xml create mode 100644 rets-io-client/src/test/resources/logback.xml create mode 100644 rets-io-client/src/test/resources/login_lower_case.xml create mode 100644 rets-io-client/src/test/resources/login_response_valid_1.0.xml create mode 100644 rets-io-client/src/test/resources/login_response_valid_1.5.xml create mode 100644 rets-io-client/src/test/resources/login_response_valid_1.7.xml create mode 100644 rets-io-client/src/test/resources/login_response_valid_1.8.xml create mode 100644 rets-io-client/src/test/resources/login_response_valid_1.9.xml create mode 100644 rets-io-client/src/test/resources/login_valid10.xml create mode 100644 rets-io-client/src/test/resources/login_valid15.xml create mode 100644 rets-io-client/src/test/resources/logout_lower_case.xml create mode 100644 rets-io-client/src/test/resources/logout_no_equals.xml create mode 100644 rets-io-client/src/test/resources/logout_valid10.xml create mode 100644 rets-io-client/src/test/resources/logout_valid15.xml create mode 100644 rets-io-client/src/test/resources/objects-missing.multipart create mode 100644 rets-io-client/src/test/resources/rets.properties create mode 100644 rets-io-common/README.md create mode 100644 rets-io-common/pom.xml create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/AttributeExtracter.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/Base64Encoder.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/Base64FormatException.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/CaseInsensitiveComparator.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/CaseInsensitiveTreeMap.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/CompactFormatData.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/CompactParser.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/DesCrypter.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/DigestUtil.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/HexUtils.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/MD5Util.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/PropertiesLocator.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/PropertiesNotFoundException.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSCompactHandler.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSConfigurator.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSReplyCodes.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSRequestResponse.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/Resource.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/ResourceLocator.java create mode 100644 rets-io-common/src/main/java/com/ossez/usreio/common/util/XMLUtils.java create mode 100644 rets-io-common/src/main/resources/log4j.properties diff --git a/rets-io-client/pom.xml b/rets-io-client/pom.xml new file mode 100644 index 0000000..109dc3a --- /dev/null +++ b/rets-io-client/pom.xml @@ -0,0 +1,266 @@ + + 4.0.0 + com.ossez.usreio + rets-io-client + 0.0.1-SNAPSHOT + jar + + + com.ossez.usreio + rets-io + 0.0.1-SNAPSHOT + + + Rets-Io-Client + The RETS API is a Reference Implementation API for RETS Transactions written in Java and can be used as + an API by web-based and desktop RETS Client Applications. + + https://github.com/USRealEstate/rets-io.git + + + + YuCheng Hu + honeymoose + huyuchengus@gmail.com + -5 + Open Source + + Sr. Java Developer + + + + + + + local + + local + /tmp/files + + + true + + + + + + + + com.ossez.usreio + rets-io-common + 0.0.1-SNAPSHOT + + + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + commons-httpclient + commons-httpclient + 3.1 + + + org.apache.ant + ant + 1.10.11 + + + org.apache.commons + commons-lang3 + 3.9 + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + + org.dom4j + dom4j + 2.1.1 + + + xalan + xalan + 2.7.2 + + + javax.activation + javax.activation-api + 1.2.0 + + + javax.mail + mail + 1.4 + + + + + junit + junit + 4.12 + test + + + org.junit.jupiter + junit-jupiter-api + 5.7.0 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.7.0 + test + + + + + + commons-codec + commons-codec + 1.3 + + + + com.google.guava + guava + 28.1-jre + + + + + + package + + + src/main/java + true + ../filtered-sources/java + + + src/main/resources + true + + + + + + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + + + + + maven-assembly-plugin + + + all-jobs + package + + single + + + trulia-java-rets-client-${env} + + jar-with-dependencies + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + + + + + + + + + + 11 + UTF-8 + UTF-8 + + + 1.7.30 + 1.2.17 + 2.2 + 1.2.5 + + + 2.11.0 + 2.6 + 3.11 + 1.4 + 4.5.13 + + + + 30.1.1-jre + + + 4.3.5.Final + + + 4.3.1.Final + + + 7.0.42 + 2.6.8 + 2.3.2 + + + 2.12.4 + + + 2.2 + 1.3 + + + 1.9.0 + 2.4.4 + 1.18.20 + 1.3 + 1.33 + 3.0.0 + 2.21.0 + 3.8.1 + 3.0.0 + + + + diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/BrokerCodeRequredException.java b/rets-io-client/src/main/java/com/ossez/usreio/client/BrokerCodeRequredException.java new file mode 100644 index 0000000..510af62 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/BrokerCodeRequredException.java @@ -0,0 +1,30 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.client; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * dbt is lame and hasn't overridden the default + * javadoc string. + */ +public class BrokerCodeRequredException extends RetsException { + private final List mCodeList; + + public BrokerCodeRequredException(Collection codes) { + this.mCodeList = Collections.unmodifiableList(new ArrayList(codes)); + } + + public List getCodeList(){ + return this.mCodeList; + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/CapabilityUrls.java b/rets-io-client/src/main/java/com/ossez/usreio/client/CapabilityUrls.java new file mode 100644 index 0000000..913f60e --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/CapabilityUrls.java @@ -0,0 +1,161 @@ +package com.ossez.usreio.client; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * + */ +public class CapabilityUrls { + + private final static Logger logger = LoggerFactory.getLogger(CapabilityUrls.class); + + public static final String ACTION_URL = "Action"; + public static final String CHANGE_PASSWORD_URL = "ChangePassword"; + public static final String GET_OBJECT_URL = "GetObject"; + public static final String LOGIN_URL = "Login"; + public static final String LOGIN_COMPLETE_URL = "LoginComplete"; + public static final String LOGOUT_URL = "Logout"; + public static final String SEARCH_URL = "Search"; + public static final String GET_METADATA_URL = "GetMetadata"; + public static final String UPDATE_URL = "Update"; + public static final String SERVER_INFO_URL = "ServerInformation";// for rets 1.7 + + private final Map mCapabilityUrls; + private URL mUrl; + + public CapabilityUrls() { + this(null); + } + + public CapabilityUrls(URL baseurl) { + this.mUrl = baseurl; + this.mCapabilityUrls = new HashMap(); + } + + public void setCapabilityUrl(String capability, String url) { + if (this.mUrl != null) { + try { + String newurl = new URL(this.mUrl, url).toString(); + if (!newurl.equals(url)) { + logger.info("qualified " + capability + " URL different: " + + url + " -> " + newurl); + url = newurl; + } + + } catch (MalformedURLException e) { + logger.warn("Couldn't normalize URL", e); + } + } + this.mCapabilityUrls.put(capability, url); + + } + + public String getCapabilityUrl(String capability) { + return (String) this.mCapabilityUrls.get(capability); + } + + public void setActionUrl(String url) { + setCapabilityUrl(ACTION_URL, url); + } + + public String getActionUrl() { + return getCapabilityUrl(ACTION_URL); + } + + public void setChangePasswordUrl(String url) { + setCapabilityUrl(CHANGE_PASSWORD_URL, url); + } + + public String getChangePasswordUrl() { + return getCapabilityUrl(CHANGE_PASSWORD_URL); + } + + public void setGetObjectUrl(String url) { + setCapabilityUrl(GET_OBJECT_URL, url); + } + + public String getGetObjectUrl() { + return getCapabilityUrl(GET_OBJECT_URL); + } + + /** + * + * @param url + */ + public void setLoginUrl(String url) { + if (this.mUrl == null) { + try { + this.mUrl = new URL(url); + } catch (MalformedURLException e) { + logger.debug("java.net.URL can't parse login url: " + url); + this.mUrl = null; + } + } + setCapabilityUrl(LOGIN_URL, url); + } + + public String getLoginUrl() { + return getCapabilityUrl(LOGIN_URL); + } + + public void setLoginCompleteUrl(String url) { + setCapabilityUrl(LOGIN_COMPLETE_URL, url); + } + + public String getLoginCompleteUrl() { + return getCapabilityUrl(LOGIN_COMPLETE_URL); + } + + public void setLogoutUrl(String url) { + setCapabilityUrl(LOGOUT_URL, url); + } + + public String getLogoutUrl() { + return getCapabilityUrl(LOGOUT_URL); + } + + public void setSearchUrl(String url) { + setCapabilityUrl(SEARCH_URL, url); + } + + public String getSearchUrl() { + return getCapabilityUrl(SEARCH_URL); + } + + public void setGetMetadataUrl(String url) { + setCapabilityUrl(GET_METADATA_URL, url); + } + + public String getGetMetadataUrl() { + return getCapabilityUrl(GET_METADATA_URL); + } + + public void setUpdateUrl(String url) { + setCapabilityUrl(UPDATE_URL, url); + } + + public String getUpdateUrl() { + return getCapabilityUrl(UPDATE_URL); + } + /** + * This is for RETS 1.7 and later and will return an empty string if it is not implemented. + * @param url + */ + public void setServerInfo(String url) { + setCapabilityUrl(SERVER_INFO_URL, url); + } + /** + * This is for RETS 1.7 and later and will return an empty string if it is not implemented. + * @return + */ + public String getServerInfo() { + return getCapabilityUrl(SERVER_INFO_URL); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/ChangePasswordRequest.java b/rets-io-client/src/main/java/com/ossez/usreio/client/ChangePasswordRequest.java new file mode 100644 index 0000000..af7ac26 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/ChangePasswordRequest.java @@ -0,0 +1,40 @@ +package com.ossez.usreio.client; + +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; + +import org.apache.commons.codec.binary.Base64; + +public class ChangePasswordRequest extends VersionInsensitiveRequest { + public ChangePasswordRequest(String username, String oldPassword, String newPassword) throws RetsException { + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(username.toUpperCase().getBytes()); + md5.update(oldPassword.toUpperCase().getBytes()); + byte[] digest = md5.digest(); + DESKeySpec keyspec = new DESKeySpec(digest); + SecretKey key = SecretKeyFactory.getInstance("DES").generateSecret(keyspec); + Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, key); + cipher.update(newPassword.getBytes()); + cipher.update(":".getBytes()); + cipher.update(username.getBytes()); + md5.reset(); + md5.update(cipher.doFinal()); + byte[] output = md5.digest(); + byte[] param = Base64.encodeBase64(output); + setQueryParameter("PWD", new String(param)); + } catch (GeneralSecurityException e) { + throw new RetsException(e); + } + } + + @Override + public void setUrl(CapabilityUrls urls) { + this.setUrl(urls.getChangePasswordUrl()); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/ChangePasswordResponse.java b/rets-io-client/src/main/java/com/ossez/usreio/client/ChangePasswordResponse.java new file mode 100644 index 0000000..9370c2e --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/ChangePasswordResponse.java @@ -0,0 +1,36 @@ +package com.ossez.usreio.client; + +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; + +import java.io.InputStream; + + +/** + * dbt is lame and hasn't overridden the default + * javadoc string. + */ +public class ChangePasswordResponse { + public ChangePasswordResponse(InputStream stream) throws RetsException { + SAXReader builder = new SAXReader(); + Document document = null; + try { + document = builder.read(stream); + } catch (Exception e) { + throw new RetsException(e); + } + Element rets = document.getRootElement(); + if (!rets.getName().equals("RETS")) { + throw new RetsException("Invalid Change Password Response"); + } + + int replyCode = Integer.parseInt(rets.attributeValue("ReplyCode")); + if (replyCode != 0) { + InvalidReplyCodeException exception; + exception = new InvalidReplyCodeException(replyCode); + exception.setRemoteMessage(rets.attributeValue("ReplyText")); + throw exception; + } + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/CollectionOfCollectionsIterator.java b/rets-io-client/src/main/java/com/ossez/usreio/client/CollectionOfCollectionsIterator.java new file mode 100644 index 0000000..da398f5 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/CollectionOfCollectionsIterator.java @@ -0,0 +1,40 @@ +package com.ossez.usreio.client; + +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +public class CollectionOfCollectionsIterator implements Iterator { + private Iterator mOuter; + private Iterator mInner; + + public CollectionOfCollectionsIterator(Collection c) { + this.mOuter = c.iterator(); + hasNext(); + } + + public boolean hasNext() { + if( this.mInner != null && this.mInner.hasNext() ) { + return true; + } + while( this.mOuter.hasNext() ){ + this.mInner = ((Collection) this.mOuter.next()).iterator(); + if( this.mInner.hasNext() ){ + return true; + } + } + return false; + } + + public Object next() { + if ( this.hasNext() ) + return this.mInner.next(); + + throw new NoSuchElementException(); + } + + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/CommonsHttpClient.java b/rets-io-client/src/main/java/com/ossez/usreio/client/CommonsHttpClient.java new file mode 100644 index 0000000..84a14ff --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/CommonsHttpClient.java @@ -0,0 +1,180 @@ +package com.ossez.usreio.client; + +import java.io.UnsupportedEncodingException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.ossez.usreio.common.util.CaseInsensitiveTreeMap; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.params.ClientPNames; +import org.apache.http.client.params.CookiePolicy; +import org.apache.http.cookie.Cookie; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; + +public class CommonsHttpClient extends RetsHttpClient { + private static final int DEFAULT_TIMEOUT = 300000; + private static final String RETS_VERSION = "RETS-Version"; + private static final String RETS_SESSION_ID = "RETS-Session-ID"; + private static final String RETS_REQUEST_ID = "RETS-Request-ID"; + private static final String USER_AGENT = "User-Agent"; + private static final String RETS_UA_AUTH_HEADER = "RETS-UA-Authorization"; + private static final String ACCEPT_ENCODING = "Accept-Encoding"; + public static final String CONTENT_ENCODING = "Content-Encoding"; + public static final String DEFLATE_ENCODINGS = "gzip,deflate"; + public static final String CONTENT_TYPE = "Content-Type"; + + public static BasicHttpParams defaultParams(int timeout) { + BasicHttpParams httpClientParams = new BasicHttpParams(); + // connection to server timeouts + HttpConnectionParams.setConnectionTimeout(httpClientParams, timeout); + HttpConnectionParams.setSoTimeout(httpClientParams, timeout); + // set to rfc 2109 as it puts the ASP (IIS) cookie _FIRST_, this is critical for interealty + httpClientParams.setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.RFC_2109); + return httpClientParams; + } + public static ThreadSafeClientConnManager defaultConnectionManager(int maxConnectionsPerRoute, int maxConnectionsTotal) { + // allows for multi threaded requests from a single client + ThreadSafeClientConnManager connectionManager = new ThreadSafeClientConnManager(); + connectionManager.setDefaultMaxPerRoute(maxConnectionsPerRoute); + connectionManager.setMaxTotal(maxConnectionsTotal); + return connectionManager; + } + + private final ConcurrentHashMap defaultHeaders; + private final DefaultHttpClient httpClient; + + // method choice improvement + private final String userAgentPassword; + + public CommonsHttpClient() { + this(new DefaultHttpClient(defaultConnectionManager(Integer.MAX_VALUE, Integer.MAX_VALUE), defaultParams(DEFAULT_TIMEOUT)), null, true); + } + + public CommonsHttpClient(int timeout, String userAgentPassword, boolean gzip) { + this(new DefaultHttpClient(defaultConnectionManager(Integer.MAX_VALUE, Integer.MAX_VALUE), defaultParams(timeout)), userAgentPassword, gzip); + } + + public CommonsHttpClient(DefaultHttpClient client, String userAgentPassword, boolean gzip) { + this.defaultHeaders = new ConcurrentHashMap(); + this.userAgentPassword = userAgentPassword; + + this.httpClient = client; + // ask the server if we can use gzip + if( gzip ) this.addDefaultHeader(ACCEPT_ENCODING, DEFLATE_ENCODINGS); + } + + public DefaultHttpClient getHttpClient(){ + return this.httpClient; + } + + //----------------------method implementations + @Override + public void setUserCredentials(String userName, String password) { + UsernamePasswordCredentials creds = new UsernamePasswordCredentials(userName, password); + this.httpClient.getCredentialsProvider().setCredentials(AuthScope.ANY, creds); + } + @Override + public RetsHttpResponse doRequest(String httpMethod, RetsHttpRequest request) throws RetsException { + return "GET".equals(StringUtils.upperCase(httpMethod)) ? this.doGet(request) : this.doPost(request); + } + + //----------------------method implementations + public RetsHttpResponse doGet(RetsHttpRequest request) throws RetsException { + String url = request.getUrl(); + String args = request.getHttpParameters(); + if (args != null) { + url = url + "?" + args; + } + HttpGet method = new HttpGet(url); + return execute(method, request.getHeaders()); + } + + public RetsHttpResponse doPost(RetsHttpRequest request) throws RetsException { + String url = request.getUrl(); + String body = request.getHttpParameters(); + if (body == null) body = ""; // commons-httpclient 3.0 refuses to accept null entity (body) + HttpPost method = new HttpPost(url); + try { + method.setEntity(new StringEntity(body, null, null)); + } catch (UnsupportedEncodingException e) { + throw new RetsException(e); + } + //updated Content-Type, application/x-www-url-encoded no longer supported + method.setHeader("Content-Type", "application/x-www-form-urlencoded"); + return execute(method, request.getHeaders()); + } + + protected RetsHttpResponse execute(final HttpRequestBase method, Map headers) throws RetsException { + try { + // add the default headers + if (this.defaultHeaders != null) { + for (Map.Entry entry : this.defaultHeaders.entrySet()) { + method.setHeader(entry.getKey(), entry.getValue()); + } + } + // add our request headers from rets + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + method.setHeader(entry.getKey(), entry.getValue()); + } + } + // optional ua-auth stuff here + if( this.userAgentPassword != null ){ + method.setHeader(RETS_UA_AUTH_HEADER, calculateUaAuthHeader(method,getCookies())); + } + // try to execute the request + HttpResponse response = this.httpClient.execute(method); + StatusLine status = response.getStatusLine(); + if (status.getStatusCode() != HttpStatus.SC_OK) { + throw new InvalidHttpStatusException(status); + } + return new CommonsHttpClientResponse(response, getCookies()); + } catch (Exception e) { + throw new RetsException(e); + } + } + + @Override + public synchronized void addDefaultHeader(String key, String value) { + this.defaultHeaders.put(key, value); + if( value == null ) this.defaultHeaders.remove(key); + } + + protected Map getCookies() { + Map cookieMap = new CaseInsensitiveTreeMap(); + for (Cookie cookie : this.httpClient.getCookieStore().getCookies()) { + cookieMap.put(cookie.getName(), cookie.getValue()); + } + return cookieMap; + } + + protected String calculateUaAuthHeader(HttpRequestBase method, Map cookies ) { + final String userAgent = this.getHeaderValue(method, USER_AGENT); + final String requestId = this.getHeaderValue(method, RETS_REQUEST_ID); + final String sessionId = cookies.get(RETS_SESSION_ID); + final String retsVersion = this.getHeaderValue(method, RETS_VERSION); + String secretHash = DigestUtils.md5Hex(String.format("%s:%s",userAgent,this.userAgentPassword)); + String pieces = String.format("%s:%s:%s:%s",secretHash,StringUtils.trimToEmpty(requestId),StringUtils.trimToEmpty(sessionId),retsVersion); + return String.format("Digest %s", DigestUtils.md5Hex(pieces)); + } + + protected String getHeaderValue(HttpRequestBase method, String key){ + Header requestHeader = method.getFirstHeader(key); + if( requestHeader == null ) return null; + return requestHeader.getValue(); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/CommonsHttpClientResponse.java b/rets-io-client/src/main/java/com/ossez/usreio/client/CommonsHttpClientResponse.java new file mode 100644 index 0000000..4236b63 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/CommonsHttpClientResponse.java @@ -0,0 +1,110 @@ +package com.ossez.usreio.client; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +import com.ossez.usreio.common.util.CaseInsensitiveTreeMap; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Header; +import org.apache.http.HttpResponse; + +import com.google.common.io.Closeables; + +public class CommonsHttpClientResponse implements RetsHttpResponse { + private HttpResponse response; + private Map headers; + private Map cookies; + + public CommonsHttpClientResponse(HttpResponse response, Map cookies) { + this.response = response; + this.cookies = new CaseInsensitiveTreeMap(cookies); + + this.headers = new CaseInsensitiveTreeMap(); + for (Header header : this.response.getAllHeaders()) { + this.headers.put(header.getName(), header.getValue()); + } + } + + public int getResponseCode() { + return this.response.getStatusLine().getStatusCode(); + } + + public Map getHeaders() { + return this.headers; + } + + public String getHeader(String header) { + return this.headers.get(header); + } + + + public Map getCookies() throws RetsException { + return this.cookies; + } + + + public String getCookie(String cookie) throws RetsException { + return this.cookies.get(cookie); + } + + + public String getCharset() { + String contentType = StringUtils.trimToEmpty(this.getHeader(CommonsHttpClient.CONTENT_TYPE)).toLowerCase(); + String[] split = StringUtils.split(contentType, ";"); + if (split == null) return null; + + for (String s : split) { + String sLower = s.toLowerCase().trim(); + boolean b = sLower.startsWith("charset="); + if (b){ + return StringUtils.substringAfter(s, "charset="); + } + } + return null; + } + + /** using this mess to provide logging, gzipping and httpmethod closing */ + + public InputStream getInputStream() throws RetsException { + try { + // get our underlying stream + InputStream inputStream = this.response.getEntity().getContent(); + // gzipped aware checks + String contentEncoding = StringUtils.trimToEmpty(this.getHeader(CommonsHttpClient.CONTENT_ENCODING)).toLowerCase(); + boolean gzipped = ArrayUtils.contains(CommonsHttpClient.DEFLATE_ENCODINGS.split(","),contentEncoding); + if( gzipped ) inputStream = new GZIPInputStream(inputStream); + + final InputStream in = inputStream; + // the http method close wrapper (necessary) + return new InputStream(){ + + public int read() throws IOException { + return in.read(); + } + + public int read(byte[] b) throws IOException { + return in.read(b); + } + + public int read(byte[] b, int off, int len) throws IOException { + return in.read(b, off, len); + } + + public void close() throws IOException { + // connection release _AFTER_ the input stream has been read + try { + Closeables.close(in, true); + } catch (IOException e) { + // ignore + } + } + }; + } catch (IOException e) { + throw new RetsException(e); + } + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/CompactRowPolicy.java b/rets-io-client/src/main/java/com/ossez/usreio/client/CompactRowPolicy.java new file mode 100644 index 0000000..3e6e316 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/CompactRowPolicy.java @@ -0,0 +1,56 @@ +package com.ossez.usreio.client; + +import org.apache.commons.logging.LogFactory; + + +public interface CompactRowPolicy { + + /** fail fast and furiously */ + public static final CompactRowPolicy STRICT = new CompactRowPolicy(){ + + public boolean apply(int row, String[] columns, String[] values) { + if( values.length != columns.length ) + throw new IllegalArgumentException(String.format("Invalid number of result columns: got %s, expected %s",values.length, columns.length)); + return true; + }}; + + /** drop everything thats suspect */ + public static final CompactRowPolicy DROP = new CompactRowPolicy(){ + + public boolean apply(int row, String[] columns, String[] values) { + if (values.length != columns.length) { + LogFactory.getLog(CompactRowPolicy.class).warn(String.format("Row %s: Invalid number of result columns: got %s, expected ",row, values.length, columns.length)); + return false; + } + return true; + }}; + + /** fail fast on long rows */ + public static final CompactRowPolicy DEFAULT = new CompactRowPolicy(){ + + public boolean apply(int row, String[] columns, String[] values) { + if (values.length > columns.length) { + throw new IllegalArgumentException(String.format("Invalid number of result columns: got %s, expected %s",values.length, columns.length)); + } + if (values.length < columns.length) { + LogFactory.getLog(CompactRowPolicy.class).warn(String.format("Row %s: Invalid number of result columns: got %s, expected ",row, values.length, columns.length)); + } + return true; + }}; + + /** drop and log long rows, try to keep short rows */ + public static final CompactRowPolicy DROP_LONG = new CompactRowPolicy(){ + + public boolean apply(int row, String[] columns, String[] values) { + if (values.length > columns.length) { + LogFactory.getLog(CompactRowPolicy.class).warn(String.format("Row %s: Invalid number of result columns: got %s, expected ",row, values.length, columns.length)); + return false; + } + if (values.length < columns.length) { + LogFactory.getLog(CompactRowPolicy.class).warn(String.format("Row %s: Invalid number of result columns: got %s, expected ",row, values.length, columns.length)); + } + return true; + }}; + + public boolean apply(int row, String[] columns, String[] values); +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/GenericHttpRequest.java b/rets-io-client/src/main/java/com/ossez/usreio/client/GenericHttpRequest.java new file mode 100644 index 0000000..9c02ac6 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/GenericHttpRequest.java @@ -0,0 +1,36 @@ +package com.ossez.usreio.client; + +/** + * on the off chance you need an ad hoc request object... + */ +public class GenericHttpRequest extends VersionInsensitiveRequest { + + public GenericHttpRequest(){ + // noop + } + + public GenericHttpRequest(String url){ + this.mUrl = url; + } + + /** + * throws an exception. GenericHttpRequest can't have a + * CapabilityUrl + * @param urls the CapabilityUrls object that has nothing we can use + */ + @Override + public void setUrl(CapabilityUrls urls) { + // do nothing + return; + } + + /** + * expose the queryParameter interface to build query arguments. + * @param name the parameter name + * @param value the parameter value + */ + @Override + public void setQueryParameter(String name, String value) { + super.setQueryParameter(name, value); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/GetMetadataRequest.java b/rets-io-client/src/main/java/com/ossez/usreio/client/GetMetadataRequest.java new file mode 100644 index 0000000..8330f21 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/GetMetadataRequest.java @@ -0,0 +1,73 @@ +package com.ossez.usreio.client; + +import org.apache.commons.lang3.StringUtils; + +public class GetMetadataRequest extends VersionInsensitiveRequest { + private static final int COMPACT_FORMAT = 0; + private static final int STANDARD_XML_FORMAT = 1; + public static final String KEY_TYPE = "Type"; + public static final String KEY_ID = "ID"; + public static final String KEY_FORMAT = "Format"; + public static final String FORMAT_STANDARD = "STANDARD-XML"; + public static final String FORMAT_STANDARD_PREFIX = "STANDARD-XML:"; + public static final String FORMAT_COMPACT = "COMPACT"; + + private int format; + private String standardXmlVersion; + + public GetMetadataRequest(String type, String id) throws RetsException { + this(type, new String[] { id }); + } + + public GetMetadataRequest(String type, String[] ids) throws RetsException { + assertValidIds(ids); + type = "METADATA-" + type; + if (type.equals("METADATA-SYSTEM") || type.equals("METADATA-RESOURCE")) { + assertIdZeroOrStar(ids); + } + + setQueryParameter(KEY_TYPE, type); + setQueryParameter(KEY_ID, StringUtils.join(ids, ":")); + setQueryParameter(KEY_FORMAT, FORMAT_STANDARD); + this.format = STANDARD_XML_FORMAT; + } + + @Override + public void setUrl(CapabilityUrls urls) { + setUrl(urls.getGetMetadataUrl()); + } + + private void assertValidIds(String[] ids) throws InvalidArgumentException { + if (ids.length == 0) { + throw new InvalidArgumentException("Expecting at least one ID"); + } + } + + private void assertIdZeroOrStar(String[] ids) throws InvalidArgumentException { + if (ids.length != 1) { + throw new InvalidArgumentException("Expecting 1 ID, but found, " + ids.length); + } + if (!ids[0].equals("0") && !ids[0].equals("*")) { + throw new InvalidArgumentException("Expecting ID of 0 or *, but was " + ids[0]); + } + } + + public void setCompactFormat() { + setQueryParameter(KEY_FORMAT, FORMAT_COMPACT); + this.format = COMPACT_FORMAT; + this.standardXmlVersion = null; + } + + public boolean isCompactFormat() { + return (this.format == COMPACT_FORMAT); + } + + public boolean isStandardXmlFormat() { + return (this.format == STANDARD_XML_FORMAT); + } + + public String getStandardXmlVersion() { + return this.standardXmlVersion; + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/GetMetadataResponse.java b/rets-io-client/src/main/java/com/ossez/usreio/client/GetMetadataResponse.java new file mode 100644 index 0000000..f8af42c --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/GetMetadataResponse.java @@ -0,0 +1,80 @@ +package com.ossez.usreio.client; + +import java.io.InputStream; +import java.util.List; + +import com.ossez.usreio.tests.common.metadata.JDomCompactBuilder; +import com.ossez.usreio.tests.common.metadata.JDomStandardBuilder; +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataException; +import org.apache.commons.lang3.math.NumberUtils; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; + + +public class GetMetadataResponse { + private MetaObject[] mMetadataObjs; + + public GetMetadataResponse(InputStream stream, boolean compact, boolean isStrict) throws RetsException { + try { + SAXReader builder = new SAXReader(); + Document document = builder.read(stream); + Element retsElement = document.getRootElement(); + if (!retsElement.getName().equals("RETS")) { + throw new RetsException("Expecting RETS"); + } + int replyCode = NumberUtils.toInt(retsElement.attributeValue("ReplyCode")); + if (ReplyCode.SUCCESS.equals(replyCode)) { + if (compact) { + handleCompactMetadata(document, isStrict); + } else { + handleStandardMetadata(document, isStrict); + } + } else if (ReplyCode.NO_METADATA_FOUND.equals(replyCode)) { + // No metadata is not an exceptional case + handleNoMetadataFound(retsElement); + } else { + InvalidReplyCodeException e = new InvalidReplyCodeException(replyCode); + e.setRemoteMessage(retsElement.attributeValue(retsElement.attributeValue("ReplyText"))); + throw e; + } + } catch (DocumentException e) { + throw new RetsException(e); + } + } + + private void handleNoMetadataFound(Element retsElement) throws RetsException { + List children = retsElement.elements(); + if (children.size() != 0) { + throw new RetsException("Expecting 0 children when results"); + } + this.mMetadataObjs = new MetaObject[0]; + } + + private void handleCompactMetadata(Document document, boolean isStrict) throws RetsException { + try { + JDomCompactBuilder builder = new JDomCompactBuilder(); + builder.setStrict(isStrict); + this.mMetadataObjs = builder.parse(document); + } catch (MetadataException e) { + throw new RetsException(e); + } + } + + private void handleStandardMetadata(Document document, boolean isStrict) throws RetsException { + try { + JDomStandardBuilder builder = new JDomStandardBuilder(); + builder.setStrict(isStrict); + this.mMetadataObjs = builder.parse(document); + } catch (MetadataException e) { + throw new RetsException(e); + } + } + + public MetaObject[] getMetadata() { + return this.mMetadataObjs; + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectIterator.java b/rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectIterator.java new file mode 100644 index 0000000..f5cdd5e --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectIterator.java @@ -0,0 +1,12 @@ +package com.ossez.usreio.client; + +import java.io.Closeable; +import java.util.Iterator; + +/** + * Iterator for SingleResoponseObjects + * @param + */ +public interface GetObjectIterator extends Closeable, Iterator{ + // noop +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectRequest.java b/rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectRequest.java new file mode 100644 index 0000000..6dceea0 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectRequest.java @@ -0,0 +1,101 @@ +package com.ossez.usreio.client; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.Map; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; + +public class GetObjectRequest extends VersionInsensitiveRequest { + public static final String KEY_RESOURCE = "Resource"; + public static final String KEY_TYPE = "Type"; + public static final String KEY_LOCATION = "Location"; + public static final String KEY_ID = "ID"; + + private final Map mMap; + + public GetObjectRequest(String resource, String type) { + this(resource, type, new String[] { "*/*" }); + } + + public GetObjectRequest(String resource, String type, String[] acceptMimeTypes) { + setQueryParameter(KEY_RESOURCE, resource); + setQueryParameter(KEY_TYPE, type); + this.mMap = new HashMap(); + setHeader("Accept", StringUtils.join(acceptMimeTypes, ", ")); + } + + @Override + public void setUrl(CapabilityUrls urls) { + setUrl(urls.getGetObjectUrl()); + } + + public void setLocationOnly(boolean flag) { + if (flag) { + setQueryParameter(KEY_LOCATION, "1"); + } else { + setQueryParameter(KEY_LOCATION, null); + } + } + + public void addObject(String resourceEntity, String id) { + if (id == null) + throw new NullPointerException("Object id should not be null. " + + "Cannot remove object already added to request."); + + Object cur = this.mMap.get(resourceEntity); + if (id.equals("*")) { + this.mMap.put(resourceEntity, id); + } else if (cur == null) { + this.mMap.put(resourceEntity, id); + } else if (cur instanceof String) { + if (ObjectUtils.equals(cur, "*")) { + return; + } + if (ObjectUtils.equals(cur, id)) { + return; + } + Set s = new HashSet(); + s.add(cur); + s.add(id); + this.mMap.put(resourceEntity, s); + } else if (cur instanceof Set) { + ((Set) cur).add(id); + } else { + /* NOTREACHED */ + throw new RuntimeException(resourceEntity + " has invalid value " + "of type " + cur.getClass().getName()); + } + setQueryParameter(KEY_ID, makeIdStr()); + } + + private String makeIdStr() { + StringBuffer id = new StringBuffer(); + Iterator iter = this.mMap.keySet().iterator(); + while (iter.hasNext()) { + String key = (String) iter.next(); + id.append(key); + Object cur = this.mMap.get(key); + if (cur instanceof String) { + id.append(":"); + id.append(cur); + } else if (cur instanceof Set) { + Iterator iter2 = ((Set) cur).iterator(); + while (iter2.hasNext()) { + String val = (String) iter2.next(); + id.append(":"); + id.append(val); + } + } else { + throw new RuntimeException(key + " has invalid value of " + "type " + cur.getClass().getName()); + } + if (iter.hasNext()) { + id.append(","); + } + } + return id.toString(); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectResponse.java b/rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectResponse.java new file mode 100644 index 0000000..99ed9ff --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectResponse.java @@ -0,0 +1,188 @@ +package com.ossez.usreio.client; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.NoSuchElementException; + +import com.ossez.usreio.common.util.CaseInsensitiveTreeMap; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.http.HeaderElement; +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicHeaderValueParser; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; + +public class GetObjectResponse{ + private static final int DEFAULT_BUFFER_SIZE = 8192; + + private final static GetObjectIterator EMPTY_OBJECT_RESPONSE_ITERATOR = new GetObjectIterator() { + public boolean hasNext() { + return false; + } + public SingleObjectResponse next() { + throw new NoSuchElementException(); + } + public void close() { + /* no op */ + } + public void remove() { + /* no op */ + } + }; + private final Map headers; + private final InputStream inputStream; + private final boolean isMultipart; + /** Indicate whether the response was empty */ + private boolean emptyResponse; + /** Indicate whether this GetObjectResponse is exhausted, i.e. has no objects */ + private boolean exhausted; + + public GetObjectResponse(Map headers, InputStream in) throws RetsException { + this.emptyResponse = false; + this.exhausted = false; + this.headers = new CaseInsensitiveTreeMap(headers); + this.isMultipart = getType().contains("multipart"); + this.inputStream = in; + + boolean isXml = getType().equals("text/xml"); + boolean containsContentId = headers.containsKey(SingleObjectResponse.CONTENT_ID); + // non multipart request that returns text/xml and does NOT have a Context-ID header, must only be a non-zero response code + boolean nonMultiPart_xml_withoutContentId = !this.isMultipart && isXml && !containsContentId; + // multipart request that returns text/xml can only be a non-zero response code + boolean multiPart_xml = this.isMultipart && isXml; + + if ( multiPart_xml || nonMultiPart_xml_withoutContentId ) { + int replyCode = 0; + try { + // GetObjectResponse is empty, because we have a Rets ReplyCode + this.emptyResponse = true; + SAXReader builder = new SAXReader(); + Document mDocument = builder.read(in); + Element root = mDocument.getRootElement(); + if (root.getName().equals("RETS")) { + replyCode = NumberUtils.toInt(root.attributeValue("ReplyCode")); + + // success + if (ReplyCode.SUCCESS.equals(replyCode)) return; + + // no object found - that's fine + if (ReplyCode.NO_OBJECT_FOUND.equals(replyCode)) return; + + throw new InvalidReplyCodeException(replyCode); + } + // no other possibilities + throw new RetsException("Malformed response [multipart="+this.isMultipart+", content-type=text/xml]. " + + "Content id did not exist in response and response was not valid rets response."); + } catch (DocumentException e) { + throw new RetsException(e); + } + } + } + + public String getType() { + return (String) this.headers.get("Content-Type"); + } + + public String getBoundary() { + String contentTypeValue = getType(); + HeaderElement[] contentType = BasicHeaderValueParser.parseElements(contentTypeValue, new BasicHeaderValueParser()); + if (contentType.length != 1) + throw new IllegalArgumentException("Multipart response appears to have a bad Content-Type: header value: " + + contentTypeValue); + + NameValuePair boundaryNV = contentType[0].getParameterByName("boundary"); + if (boundaryNV == null) + return null; + return unescapeBoundary(boundaryNV.getValue()); + } + + private static String unescapeBoundary(String boundaryValue) { + if (boundaryValue.startsWith("\"")) + boundaryValue = boundaryValue.substring(1); + if (boundaryValue.endsWith("\"")) + boundaryValue = boundaryValue.substring(0, boundaryValue.length() - 1); + return boundaryValue; + } + + + /** + * @return GetObjectIterator, which iterates over SingleObjectResponse + * objects. + * + * @throws RetsException + */ + public GetObjectIterator iterator() throws RetsException { + return iterator(DEFAULT_BUFFER_SIZE); + } + + /** + * @return GetObjectIterator, which iterates over SingleObjectResponse + * objects. + * + * @param bufferSize How large a buffer should be used for underlying + * streams. + * + * @throws RetsException + */ + public GetObjectIterator iterator(int bufferSize) throws RetsException { + if(this.exhausted ) + throw new RetsException("response was exhausted - cannot request iterator a second time"); + + if( this.emptyResponse ) + return (GetObjectIterator) EMPTY_OBJECT_RESPONSE_ITERATOR; + + + if( this.isMultipart ){ + try { + return GetObjectResponseIterator.createIterator(this, bufferSize); + } catch (Exception e) { + throw new RetsException("Error creating multipart GetObjectIterator", e); + } + } + // no other possibilities + return new NonMultipartGetObjectResponseIterator(this.headers, this.inputStream); + } + + public InputStream getInputStream() { + return this.inputStream; + } + +} + + +/** Used to implement GetObjectIterator for a non multipart response. */ +final class NonMultipartGetObjectResponseIterator implements GetObjectIterator { + private boolean exhausted; + private final Map headers; + private final InputStream inputStream; + + public NonMultipartGetObjectResponseIterator(Map headers, InputStream in){ + this.exhausted = false; + this.headers = headers; + this.inputStream = in; + } + + + public void close() throws IOException { + this.inputStream.close(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public boolean hasNext() { + return !this.exhausted; + } + + public SingleObjectResponse next() { + if( this.exhausted ) + throw new NoSuchElementException("stream exhausted"); + + this.exhausted = true; + return new SingleObjectResponse(this.headers, this.inputStream); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectResponseIterator.java b/rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectResponseIterator.java new file mode 100644 index 0000000..4c6fde1 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/GetObjectResponseIterator.java @@ -0,0 +1,140 @@ +package com.ossez.usreio.client; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.PushbackInputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +import org.apache.commons.lang3.StringUtils; + +public class GetObjectResponseIterator implements GetObjectIterator { + public static final char CR = '\r'; + public static final char LF = '\n'; + public static final String EOL = CR+""+LF; + public static final String BS = "--"; + + private final PushbackInputStream multipartStream; + private final String boundary; + private Boolean hasNext; + + public static GetObjectIterator createIterator(final GetObjectResponse response, int streamBufferSize) throws Exception { + String boundary = response.getBoundary(); + if (boundary != null) + return new GetObjectResponseIterator(response, boundary, streamBufferSize); + + return new GetObjectIterator() { + + public void close() throws IOException{ + response.getInputStream().close(); + } + + public boolean hasNext() { + return false; + } + + public T next() { + throw new NoSuchElementException(); + } + + public void remove() { + throw new UnsupportedOperationException(""); + } + }; + } + + private GetObjectResponseIterator(GetObjectResponse response, String boundary, int streamBufferSize) throws Exception { + this.boundary = boundary; + + BufferedInputStream input = new BufferedInputStream(response.getInputStream(), streamBufferSize); + this.multipartStream = new PushbackInputStream(input, BS.length() + this.boundary.length() + EOL.length()); + } + + + public boolean hasNext() { + if (this.hasNext != null) + return this.hasNext.booleanValue(); + + try { + this.hasNext = new Boolean(this.getHaveNext()); + return this.hasNext.booleanValue(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public T next() { + if (!this.hasNext()) + throw new NoSuchElementException(); + + this.hasNext = null; + try { + return getNext(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + public void remove() { + throw new UnsupportedOperationException(); + } + + + public void close() throws IOException { + this.multipartStream.close(); + } + + private boolean getHaveNext() throws IOException { + String line = null; + while ((line = this.readLine()) != null) { + if (line.equals(BS+this.boundary)) + return true; + if (line.equals(BS+this.boundary+BS)) + return false; + } + return false; + } + + private T getNext() throws Exception { + Map headers = new HashMap(); + String header = null; + while (StringUtils.isNotEmpty(header = this.readLine())) { + int nvSeperatorIndex = header.indexOf(':'); + if (nvSeperatorIndex == -1){ + headers.put(header, ""); + } else { + String name = header.substring(0, nvSeperatorIndex); + String value = header.substring(nvSeperatorIndex + 1).trim(); + headers.put(name, value); + } + } + return (T)new SingleObjectResponse(headers, new SinglePartInputStream(this.multipartStream, BS+this.boundary)); + } + + // TODO find existing library to do this + private String readLine() throws IOException { + boolean eolReached = false; + StringBuffer line = new StringBuffer(); + int currentChar = -1; + while (!eolReached && (currentChar = this.multipartStream.read()) != -1) { + eolReached = (currentChar == CR || currentChar == LF); + if (!eolReached) + line.append((char) currentChar); + } + + if (currentChar == -1 && line.length() == 0) + return null; + + if (currentChar == CR) { + int nextChar = this.multipartStream.read(); + if (nextChar != LF) + this.multipartStream.unread(new byte[] { (byte) nextChar }); + } + + return line.toString(); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/InvalidArgumentException.java b/rets-io-client/src/main/java/com/ossez/usreio/client/InvalidArgumentException.java new file mode 100644 index 0000000..9c9d510 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/InvalidArgumentException.java @@ -0,0 +1,7 @@ +package com.ossez.usreio.client; + +public class InvalidArgumentException extends RetsException { + public InvalidArgumentException(String message) { + super(message); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/InvalidHttpStatusException.java b/rets-io-client/src/main/java/com/ossez/usreio/client/InvalidHttpStatusException.java new file mode 100644 index 0000000..e318c72 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/InvalidHttpStatusException.java @@ -0,0 +1,12 @@ +package com.ossez.usreio.client; + +import org.apache.http.StatusLine; + +public class InvalidHttpStatusException extends RetsException { + public InvalidHttpStatusException(StatusLine status) { + super("Status code (" + status.getStatusCode() + ") " + status.getReasonPhrase()); + } + public InvalidHttpStatusException(StatusLine status, String message) { + super("Status code (" + status.getStatusCode() + ") " + status.getReasonPhrase() +" '"+message+"'"); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/InvalidReplyCodeException.java b/rets-io-client/src/main/java/com/ossez/usreio/client/InvalidReplyCodeException.java new file mode 100644 index 0000000..39bfd77 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/InvalidReplyCodeException.java @@ -0,0 +1,44 @@ +package com.ossez.usreio.client; + +import org.apache.commons.lang3.SystemUtils; +/** + * Exception class for invalid reply codes from a Rets server + */ +public class InvalidReplyCodeException extends RetsException { + private final ReplyCode mReplyCode; + private String mMsg; + private String mReqinfo; + + public InvalidReplyCodeException(int replyCodeValue) { + this.mReplyCode = ReplyCode.fromValue(replyCodeValue); + } + + public InvalidReplyCodeException(ReplyCode replyCode) { + this.mReplyCode = replyCode; + } + + @Override + public String getMessage() { + StringBuffer sb = new StringBuffer(this.mReplyCode.toString()); + if (this.mMsg != null) { + sb.append(SystemUtils.LINE_SEPARATOR + this.mMsg); + } + if (this.mReqinfo != null) { + sb.append(SystemUtils.LINE_SEPARATOR + this.mReqinfo); + } + return sb.toString(); + } + + public int getReplyCodeValue() { + return this.mReplyCode.getValue(); + } + + public void setRemoteMessage(String msg) { + this.mMsg = msg; + } + + public void setRequestInfo(String reqinfo) { + this.mReqinfo = reqinfo; + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/InvalidReplyCodeHandler.java b/rets-io-client/src/main/java/com/ossez/usreio/client/InvalidReplyCodeHandler.java new file mode 100644 index 0000000..4af39c3 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/InvalidReplyCodeHandler.java @@ -0,0 +1,17 @@ +package com.ossez.usreio.client; + +public interface InvalidReplyCodeHandler { + InvalidReplyCodeHandler FAIL = new InvalidReplyCodeHandler() { + public void invalidRetsReplyCode(int replyCode) throws InvalidReplyCodeException { + throw new InvalidReplyCodeException(replyCode); + } + + public void invalidRetsStatusReplyCode(int replyCode) throws InvalidReplyCodeException { + throw new InvalidReplyCodeException(replyCode); + } + }; + + public void invalidRetsReplyCode(int replyCode) throws InvalidReplyCodeException; + + public void invalidRetsStatusReplyCode(int replyCode) throws InvalidReplyCodeException; +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/KeyValueResponse.java b/rets-io-client/src/main/java/com/ossez/usreio/client/KeyValueResponse.java new file mode 100644 index 0000000..2ea6bd2 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/KeyValueResponse.java @@ -0,0 +1,140 @@ +package com.ossez.usreio.client; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.commons.logging.Log; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import static com.ossez.usreio.client.CapabilityUrls.LOGIN_URL; + +/** + * Process key and Value + * + * @author YuCheng Hu + */ +abstract public class KeyValueResponse { + protected static final String CRLF = "\r\n"; + private static final Logger logger = LoggerFactory.getLogger(KeyValueResponse.class); + + protected Document mDoc; + protected int mReplyCode; + protected boolean mStrict; + + public KeyValueResponse() { + this.mStrict = false; + } + + public void parse(InputStream stream, RetsVersion mVersion) throws RetsException { + try { + SAXReader builder = new SAXReader(); + this.mDoc = builder.read(stream); + Element retsElement = this.mDoc.getRootElement(); + if (!retsElement.getName().equals("RETS")) { + throw new RetsException("Expecting RETS"); + } + + int replyCode = NumberUtils.toInt(retsElement.attributeValue("ReplyCode")); + this.mReplyCode = replyCode; + if (!isValidReplyCode(replyCode)) { + throw new InvalidReplyCodeException(replyCode); + } + Element capabilityContainer; + if (RetsVersion.RETS_10.equals(mVersion)) { + capabilityContainer = retsElement; + } else { + List children = retsElement.elements(); + if (children.size() != 1) { + throw new RetsException("Invalid number of children: " + children.size()); + } + + capabilityContainer = (Element) children.get(0); + + if (!capabilityContainer.getName().equals("RETS-RESPONSE")) { + throw new RetsException("Expecting RETS-RESPONSE"); + } + } + this.handleRetsResponse(capabilityContainer); + } catch (DocumentException e) { + throw new RetsException(e); + } + } + + protected boolean isValidReplyCode(int replyCode) { + return (ReplyCode.SUCCESS.equals(replyCode)); + + } + + /** + * handleRetsResponse + * + * @param retsResponse + * @throws RetsException + */ + private void handleRetsResponse(Element retsResponse) throws RetsException { + List tokenizeList = Arrays.asList(StringUtils.split(retsResponse.getText(), CRLF)); + HashMap retsResponseMap = new HashMap(); + + for (String keyValueStr : tokenizeList) { + + String[] splits = StringUtils.split(keyValueStr, "="); + if (!ArrayUtils.isEmpty(splits) && splits.length > 1) { + String key = StringUtils.trimToNull(splits[0]); + String value = StringUtils.trimToEmpty(splits[1]); + + // PROCESS LOGIN_URL + if (StringUtils.equalsIgnoreCase(LOGIN_URL, key)) { + retsResponseMap.put(LOGIN_URL, value); + this.handleKeyValue(LOGIN_URL, value); + } else + retsResponseMap.put(key, value); + } + + } + + retsResponseMap.entrySet().parallelStream().forEach(entry -> { + try { + this.handleKeyValue(entry.getKey(), entry.getValue()); + } catch (RetsException ex) { + logger.warn("Unable process rests login response value", ex); + } + }); + + + } + + protected abstract void handleKeyValue(String key, String value) throws RetsException; + + public void setStrict(boolean strict) { + this.mStrict = strict; + } + + public boolean isStrict() { + return this.mStrict; + } + + protected boolean matchKey(String key, String value) { + if (this.mStrict) + return key.equals(value); + + return key.equalsIgnoreCase(value); + } + + protected void assertStrictWarning(Log log, String message) throws RetsException { + if (this.mStrict) + throw new RetsException(message); + + log.warn(message); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/LoginRequest.java b/rets-io-client/src/main/java/com/ossez/usreio/client/LoginRequest.java new file mode 100644 index 0000000..1576132 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/LoginRequest.java @@ -0,0 +1,23 @@ +package com.ossez.usreio.client; + +public class LoginRequest extends VersionInsensitiveRequest { + + @Override + public void setUrl(CapabilityUrls urls) { + setUrl(urls.getLoginUrl()); + } + + public void setBrokerCode(String code, String branch) { + if (code == null) { + setQueryParameter(KEY_BROKERCODE, null); + } else { + if (branch == null) { + setQueryParameter(KEY_BROKERCODE, code); + } else { + setQueryParameter(KEY_BROKERCODE, code + "," + branch); + } + } + } + + public static final String KEY_BROKERCODE = "BrokerCode"; +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/LoginResponse.java b/rets-io-client/src/main/java/com/ossez/usreio/client/LoginResponse.java new file mode 100644 index 0000000..4002463 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/LoginResponse.java @@ -0,0 +1,200 @@ +package com.ossez.usreio.client; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashSet; +import java.util.Set; + +/** + * @author YuCheng Hu + */ +public class LoginResponse extends KeyValueResponse { + private static final String BROKER_KEY = "Broker"; + private static final String MEMBER_NAME_KEY = "MemberName"; + private static final String METADATA_VER_KEY = "MetadataVersion"; + private static final String MIN_METADATA_VER_KEY = "MinMetadataVersion"; + private static final String USER_INFO_KEY = "User"; + private static final String OFFICE_LIST_KEY = "OfficeList"; + private static final String BALANCE_KEY = "Balance"; + private static final String TIMEOUT_KEY = "TimeoutSeconds"; + private static final String PWD_EXPIRE_KEY = "Expr"; + private static final String METADATA_TIMESTAMP_KEY = "MetadataTimestamp"; + private static final String MIN_METADATA_TIMESTAMP_KEY = "MinMetadataTimestamp"; + private static final Log LOG = LogFactory.getLog(LoginResponse.class); + + private String sessionId; + private String memberName; + private String userInformation; + private String broker; + private String metadataVersion; + private String minMetadataVersion; + private String metadataTimestamp; + private String minMetadataTimestamp; + private String officeList; + private String balance; + private int sessionTimeout; + private String passwordExpiration; + private CapabilityUrls capabilityUrls; + private Set brokerCodes; + + public LoginResponse(String loginUrl) { + super(); + this.brokerCodes = new HashSet(); + URL url = null; + try { + url = new URL(loginUrl); + } catch (MalformedURLException e) { + LOG.warn("Bad URL: " + loginUrl); + } + this.capabilityUrls = new CapabilityUrls(url); + } + + public LoginResponse() { + super(); + this.capabilityUrls = new CapabilityUrls(); + } + + @Override + public void parse(InputStream stream, RetsVersion version) throws RetsException { + super.parse(stream, version); + if (ReplyCode.BROKER_CODE_REQUIRED.equals(this.mReplyCode)) { + throw new BrokerCodeRequredException(this.brokerCodes); + } + } + + @Override + protected boolean isValidReplyCode(int replyCode) { + return (super.isValidReplyCode(replyCode) || ReplyCode.BROKER_CODE_REQUIRED.equals(replyCode)); + } + + @Override + protected void handleKeyValue(String key, String value) throws RetsException { + if (ReplyCode.BROKER_CODE_REQUIRED.equals(this.mReplyCode)) { + if (matchKey(key, BROKER_KEY)) { + String[] strings = StringUtils.split(value, ","); + if (strings.length > 0 && strings.length < 3) { + this.brokerCodes.add(strings); + } else { + throw new RetsException("Invalid broker/branch code: " + value); + } + } + } + + if (matchKey(key, BROKER_KEY)) { + this.broker = value; + } else if (matchKey(key, MEMBER_NAME_KEY)) { + this.memberName = value; + } else if (matchKey(key, METADATA_VER_KEY)) { + this.metadataVersion = value; + } else if (matchKey(key, MIN_METADATA_VER_KEY)) { + this.minMetadataVersion = value; + } else if (matchKey(key, METADATA_TIMESTAMP_KEY)) { + this.metadataTimestamp = value; + } else if (matchKey(key, MIN_METADATA_TIMESTAMP_KEY)) { + this.minMetadataTimestamp = value; + } else if (matchKey(key, USER_INFO_KEY)) { + this.userInformation = value; + } else if (matchKey(key, OFFICE_LIST_KEY)) { + this.officeList = value; + } else if (matchKey(key, BALANCE_KEY)) { + this.balance = value; + } else if (matchKey(key, TIMEOUT_KEY)) { + this.sessionTimeout = NumberUtils.toInt(value); + } else if (matchKey(key, PWD_EXPIRE_KEY)) { + this.passwordExpiration = value; + } else if (matchKey(key, CapabilityUrls.ACTION_URL)) { + this.capabilityUrls.setActionUrl(value); + } else if (matchKey(key, CapabilityUrls.CHANGE_PASSWORD_URL)) { + this.capabilityUrls.setChangePasswordUrl(value); + } else if (matchKey(key, CapabilityUrls.GET_OBJECT_URL)) { + this.capabilityUrls.setGetObjectUrl(value); + } else if (matchKey(key, CapabilityUrls.LOGIN_URL)) { + this.capabilityUrls.setLoginUrl(value); + } else if (matchKey(key, CapabilityUrls.LOGIN_COMPLETE_URL)) { + this.capabilityUrls.setLoginCompleteUrl(value); + } else if (matchKey(key, CapabilityUrls.LOGOUT_URL)) { + this.capabilityUrls.setLogoutUrl(value); + } else if (matchKey(key, CapabilityUrls.SEARCH_URL)) { + this.capabilityUrls.setSearchUrl(value); + } else if (matchKey(key, CapabilityUrls.GET_METADATA_URL)) { + this.capabilityUrls.setGetMetadataUrl(value); + } else if (matchKey(key, CapabilityUrls.UPDATE_URL)) { + this.capabilityUrls.setUpdateUrl(value); + } else if (matchKey(key, CapabilityUrls.SERVER_INFO_URL)) { + this.capabilityUrls.setServerInfo(value); + LOG.warn("Depreciated: " + key + " -> " + value); + } else if (matchKey(key, "Get")) { + LOG.warn("Found bad key: Get -> " + value); + // FIX ME: Should not get this + } else { + if (key.substring(0, 2).equalsIgnoreCase("X-")) { + LOG.warn("Unknown experimental key: " + key + " -> " + value); + } else { + assertStrictWarning(LOG, "Invalid login response key: " + key + " -> " + value); + } + } + } + + public String getMemberName() { + return this.memberName; + } + + public String getUserInformation() { + return this.userInformation; + } + + public String getBroker() { + return this.broker; + } + + public String getMetadataVersion() { + return this.metadataVersion; + } + + public String getMinMetadataVersion() { + return this.minMetadataVersion; + } + + public String getMetadataTimestamp() { + return this.metadataTimestamp; + } + + public String getMinMetadataTimestamp() { + return this.minMetadataTimestamp; + } + + public String getOfficeList() { + return this.officeList; + } + + public String getBalance() { + return this.balance; + } + + public int getSessionTimeout() { + return this.sessionTimeout; + } + + public String getPasswordExpiration() { + return this.passwordExpiration; + } + + public CapabilityUrls getCapabilityUrls() { + return this.capabilityUrls; + } + + public String getSessionId() { + return this.sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/LogoutRequest.java b/rets-io-client/src/main/java/com/ossez/usreio/client/LogoutRequest.java new file mode 100644 index 0000000..30a1e43 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/LogoutRequest.java @@ -0,0 +1,9 @@ +package com.ossez.usreio.client; + +public class LogoutRequest extends VersionInsensitiveRequest { + + @Override + public void setUrl(CapabilityUrls urls) { + setUrl(urls.getLogoutUrl()); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/LogoutResponse.java b/rets-io-client/src/main/java/com/ossez/usreio/client/LogoutResponse.java new file mode 100644 index 0000000..02cb16b --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/LogoutResponse.java @@ -0,0 +1,41 @@ +package com.ossez.usreio.client; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class LogoutResponse extends KeyValueResponse { + private static final Log LOG = LogFactory.getLog(LogoutResponse.class); + private static final String CONNECT_TIME_KEY = "ConnectTime"; + private static final String BILLING_KEY = "Billing"; + private static final String SIGNOFF_KEY = "SignOffMessage"; + + private String seconds; + private String billingInfo; + private String logoutMessage; + + @Override + protected void handleKeyValue(String key, String value) throws RetsException { + if (matchKey(key, CONNECT_TIME_KEY)) { + this.seconds = value; + } else if (matchKey(key, BILLING_KEY)) { + this.billingInfo = value; + } else if (matchKey(key, SIGNOFF_KEY)) { + this.logoutMessage = value; + } else { + assertStrictWarning(LOG, "Invalid logout response key: " + key + " -> " + value); + } + } + + public String getSeconds() { + return this.seconds; + } + + public String getBillingInfo() { + return this.billingInfo; + } + + public String getLogoutMessage() { + return this.logoutMessage; + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/MetaCollectorAdapter.java b/rets-io-client/src/main/java/com/ossez/usreio/client/MetaCollectorAdapter.java new file mode 100644 index 0000000..282a9bc --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/MetaCollectorAdapter.java @@ -0,0 +1,61 @@ +package com.ossez.usreio.client; + +import com.ossez.usreio.tests.common.metadata.MetaCollector; +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public abstract class MetaCollectorAdapter implements MetaCollector { + + + public MetaObject[] getMetadata(MetadataType type, String path) { + return getSome(type, path, "0"); + } + + + public MetaObject[] getMetadataRecursive(MetadataType type, String path) { + return getSome(type, path, "*"); + } + + private MetaObject[] getSome(MetadataType type, String path, String sfx) { + boolean compact = Boolean.getBoolean("rets-client.metadata.compact"); + try { + GetMetadataRequest req; + if (path == null || path.equals("")) { + req = new GetMetadataRequest(type.name(), sfx); + } else { + String[] ppath = StringUtils.split(path, ":"); + String[] id = new String[ppath.length + 1]; + System.arraycopy(ppath, 0, id, 0, ppath.length); + id[ppath.length] = sfx; + req = new GetMetadataRequest(type.name(), id); + } + if (compact) { + req.setCompactFormat(); + } + GetMetadataResponse response; + + response = doRequest(req); + + return response.getMetadata(); + } catch (RetsException e) { + LOG.error("bad metadata request", e); + return null; + } + } + + /** + * Perform operation of turning a GetMetadataRequest into + * a GetMetadataResponse + * + * @param req Requested metadata + * @return parsed MetaObjects + * + * @throws RetsException if an error occurs + */ + protected abstract GetMetadataResponse doRequest(GetMetadataRequest req) throws RetsException; + + private static final Log LOG = LogFactory.getLog(MetaCollectorAdapter.class); +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/MetaCollectorImpl.java b/rets-io-client/src/main/java/com/ossez/usreio/client/MetaCollectorImpl.java new file mode 100644 index 0000000..1c881d4 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/MetaCollectorImpl.java @@ -0,0 +1,22 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.client; + +public class MetaCollectorImpl extends MetaCollectorAdapter { + private final RetsTransport mTransport; + + public MetaCollectorImpl(RetsTransport transport) { + this.mTransport = transport; + } + + @Override + protected GetMetadataResponse doRequest(GetMetadataRequest req) throws RetsException { + return this.mTransport.getMetadata(req); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/NetworkEventMonitor.java b/rets-io-client/src/main/java/com/ossez/usreio/client/NetworkEventMonitor.java new file mode 100644 index 0000000..04ffe4b --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/NetworkEventMonitor.java @@ -0,0 +1,23 @@ +package com.ossez.usreio.client; + +/** + * A client can register a monitor for network events + */ +public interface NetworkEventMonitor +{ + /** + * inform the client app that an event has started. + * the client app can return an object, which will be passed + * to eventFinish(). + * + * @param message a message describing the event + * @return an object to be passed to eventFinish, or null + */ + public Object eventStart(String message); + /** + * Inform the client app that the previous event has completed + * + * @param o the object returned from eventStart + */ + public void eventFinish(Object o); +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/NullNetworkEventMonitor.java b/rets-io-client/src/main/java/com/ossez/usreio/client/NullNetworkEventMonitor.java new file mode 100644 index 0000000..82526e5 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/NullNetworkEventMonitor.java @@ -0,0 +1,13 @@ +package com.ossez.usreio.client; + +public class NullNetworkEventMonitor implements NetworkEventMonitor { + + public Object eventStart(String message) { + return null; + } + + + public void eventFinish(Object o) { + //noop + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/ReplyCode.java b/rets-io-client/src/main/java/com/ossez/usreio/client/ReplyCode.java new file mode 100644 index 0000000..82ac9bb --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/ReplyCode.java @@ -0,0 +1,100 @@ +package com.ossez.usreio.client; + +import java.util.Map; +import java.util.HashMap; + +public class ReplyCode { + // static initialization loop.... this declaration _MUST_ come before the members + private static final Map CODES = new HashMap(); + + public static final ReplyCode SUCCESS = new ReplyCode(0, "Success"); + public static final ReplyCode ZERO_BALANCE = new ReplyCode(20003, "Zero balance"); + public static final ReplyCode BROKER_CODE_REQUIRED = new ReplyCode(20012, "Broker code required"); + public static final ReplyCode BROKER_CODE_INVALID = new ReplyCode(20013, "Broker Code Invalid"); + public static final ReplyCode ADDTIONAL_LOGIN_NOT_PREMITTED = new ReplyCode(20022, "Additional login not permitted"); + public static final ReplyCode MISCELLANEOUS_LOGIN_ERROR = new ReplyCode(20036, "Miscellaneous server login error"); + public static final ReplyCode CLIENT_PASSWORD_INVALID = new ReplyCode(20037, "Client passsword invalid"); + public static final ReplyCode SERVER_TEMPORARILY_DISABLED = new ReplyCode(20050, "Server temporarily disabled"); + public static final ReplyCode UNKNOWN_QUERY_FIELD = new ReplyCode(20200, "Unknown Query Field"); + public static final ReplyCode NO_RECORDS_FOUND = new ReplyCode(20201, "No Records Found"); + public static final ReplyCode INVALID_SELECT = new ReplyCode(20202, "Invalid select"); + public static final ReplyCode MISCELLANOUS_SEARCH_ERROR = new ReplyCode(20203, "Miscellaneous search error"); + public static final ReplyCode INVALID_QUERY_SYNTAX = new ReplyCode(20206, "Invalid query syntax"); + public static final ReplyCode UNAUTHORIZED_QUERY = new ReplyCode(20207, "Unauthorized query"); + public static final ReplyCode MAXIMUM_RECORDS_EXCEEDED = new ReplyCode(20208, "Maximum records exceeded"); + public static final ReplyCode SEARCH_TIMED_OUT = new ReplyCode(20209, "Search timed out"); + public static final ReplyCode TOO_MANY_OUTSTANDING_QUERIES = new ReplyCode(20210, "Too many outstanding queries"); + public static final ReplyCode INVALID_RESOURCE_GETOBJECT = new ReplyCode(20400, "Invalid Resource"); + public static final ReplyCode INVALID_TYPE_GETOBJECT = new ReplyCode(20401, "Invalid Type"); + public static final ReplyCode INVALID_IDENTIFIER_GETOBJECT = new ReplyCode(20402, "Invalid Identifier"); + public static final ReplyCode NO_OBJECT_FOUND = new ReplyCode(20403, "No Object Found"); + public static final ReplyCode UNSUPPORTED_MIME_TYPE_GETOBJECT = new ReplyCode(20406, "Unsupported MIME Type"); + public static final ReplyCode UNAUTHORIZED_RETRIEVAL_GETOBJECT = new ReplyCode(20407, "Unauthorized Retrieval"); + public static final ReplyCode RESOURCE_UNAVAILABLE_GETOBJECT = new ReplyCode(20408, "Resource Unavailable"); + public static final ReplyCode OBJECT_UNAVAILABLE = new ReplyCode(20409, "Object Unavailable"); + public static final ReplyCode REQUEST_TOO_LARGE_GETOBJECT = new ReplyCode(20410, "Request Too Large"); + public static final ReplyCode TIMEOUT_GETOBJECT = new ReplyCode(20411, "Timeout"); + public static final ReplyCode TOO_MANY_OUTSTANDING_QUERIES_GETOBJECT = new ReplyCode(20412,"Too Many Outstanding Queries"); + public static final ReplyCode MISCELLANEOUS_ERROR_GETOBJECT = new ReplyCode(20413, "Miscellaneous Error"); + public static final ReplyCode INVALID_RESOURCE = new ReplyCode(20500, "Invalid resource"); + public static final ReplyCode INVALID_TYPE = new ReplyCode(20501, "Invalid type"); + public static final ReplyCode INVALID_IDENTIFIER = new ReplyCode(20502, "Invalid identifier"); + public static final ReplyCode NO_METADATA_FOUND = new ReplyCode(20503, "No metadata found"); + public static final ReplyCode UNSUPPORTED_MIME_TYPE = new ReplyCode(20506, "Unsupported MIME type"); + public static final ReplyCode UNAUTHORIZED_RETRIEVAL = new ReplyCode(20507, "Unauthorized retrieval"); + public static final ReplyCode RESOURCE_UNAVAILABLE = new ReplyCode(20508, "Resource unavailable"); + public static final ReplyCode METADATA_UNAVAILABLE = new ReplyCode(20509, "Metadata unavailable"); + public static final ReplyCode REQUEST_TOO_LARGE = new ReplyCode(20510, "Request too large"); + public static final ReplyCode TIMEOUT = new ReplyCode(20511, "Timeout"); + public static final ReplyCode TOO_MANY_OUSTANDING_REQUESTS = new ReplyCode(20512, "Too many outstanding requests"); + public static final ReplyCode MISCELLANEOUS_ERROR = new ReplyCode(20513, "Miscellanous error"); + public static final ReplyCode REQUESTED_DTD_UNAVAILABLE = new ReplyCode(20514, "Requested DTD unvailable"); + + private final int mValue; + private final String mMessage; + + private ReplyCode(int value, String message) { + this.mValue = value; + this.mMessage = message; + if (CODES.containsValue(new Integer(value))) + throw new IllegalArgumentException(String.format("value already used: %s ( %s ) ",value,message)); + CODES.put(new Integer(value), this); + } + + + @Override + public boolean equals(Object o) { + if (!(o instanceof ReplyCode)) { + return false; + } + + ReplyCode rhs = (ReplyCode) o; + return (this.mValue == rhs.mValue); + } + + public boolean equals(int value) { + return this.mValue == value; + } + + @Override + public String toString() { + return String.format("%s (%s)",this.mValue,this.mMessage); + } + + public int getValue() { + return this.mValue; + } + + public String getMessage() { + return this.mMessage; + } + + public static ReplyCode fromValue(int value) { + ReplyCode replyCode = CODES.get(new Integer(value)); + if (replyCode != null) + return replyCode; + + return new ReplyCode(value, "Unknown"); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/ReplyCodeHandler.java b/rets-io-client/src/main/java/com/ossez/usreio/client/ReplyCodeHandler.java new file mode 100644 index 0000000..d99c1b9 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/ReplyCodeHandler.java @@ -0,0 +1,23 @@ +package com.ossez.usreio.client; + +/** + * @author jrayburn + */ +public interface ReplyCodeHandler { + + /** + * ReplyCodeHandler can choose to handle reply codes + * that are non-zero reply codes in its own fashion. + * + * This is intended to be used to allow the SearchResultCollector + * to choose to throw InvalidReplyCodeException if the response is + * 20201 (Empty) or 20208 (MaxRowsExceeded). + * + * @param replyCode The RETS reply code + * + * @throws InvalidReplyCodeException Thrown if reply code is + * invalid for the SearchResultCollector. + */ + public void handleReplyCode(int replyCode) throws InvalidReplyCodeException; + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/RetsException.java b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsException.java new file mode 100644 index 0000000..d115159 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsException.java @@ -0,0 +1,22 @@ +package com.ossez.usreio.client; + +/** + * @author YuCheng Hu + */ +public class RetsException extends Exception { + public RetsException() { + super(); + } + + public RetsException(String message) { + super(message); + } + + public RetsException(String message, Throwable cause) { + super(message, cause); + } + + public RetsException(Throwable cause) { + super(cause); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/RetsHttpClient.java b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsHttpClient.java new file mode 100644 index 0000000..0c77aae --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsHttpClient.java @@ -0,0 +1,36 @@ +package com.ossez.usreio.client; + + +public abstract class RetsHttpClient { + + public static final String SESSION_ID_COOKIE = "RETS-Session-ID"; + public static final String LOGIN_SESSION_ID = "0"; + + public abstract void setUserCredentials(String userName, String password); + + /** + * The protocol specific implementation happens here. + * + * @param httpMethod + * @param request + * @return + * @throws RetsException + */ + public abstract RetsHttpResponse doRequest(String httpMethod, RetsHttpRequest request) throws RetsException; + + /** + * + * + * @param name header name, case should be preserved + * @param value static header value, if null then implementation should not include the header in requests + */ + + /** + * Add an HTTP header that should be included by default in all requests + * + * @param name header name, case should be preserved + * @param value static header value, if null then implementation should not include the header in requests + */ + public abstract void addDefaultHeader(String name, String value); + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/RetsHttpRequest.java b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsHttpRequest.java new file mode 100644 index 0000000..f954583 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsHttpRequest.java @@ -0,0 +1,81 @@ +package com.ossez.usreio.client; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import com.ossez.usreio.common.util.CaseInsensitiveTreeMap; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** Base Http Request object */ +public abstract class RetsHttpRequest implements Serializable { + private final Map mHeaders; + private final SortedMap mQueryParameters; + protected String mUrl; + + public RetsHttpRequest() { + this.mHeaders = new CaseInsensitiveTreeMap(); + this.mQueryParameters = new TreeMap(); + } + + public abstract void setUrl(CapabilityUrls urls); + + public void setUrl(String url) { + this.mUrl = url; + } + + public String getUrl() { + return this.mUrl; + } + + public void setHeader(String key, String value) { + this.mHeaders.put(key, value); + } + + public Map getHeaders() { + return this.mHeaders; + } + + public String getHttpParameters() { + if (this.mQueryParameters.isEmpty()) + return null; + + List params = new LinkedList(); + for(Map.Entry param : this.mQueryParameters.entrySet()){ + params.add(String.format("%s=%s",RetsUtil.urlEncode(param.getKey()),RetsUtil.urlEncode(param.getValue()))); + } + return StringUtils.join(params.iterator(),"&"); + } + + protected void setQueryParameter(String name, String value) { + if (value == null) { + this.mQueryParameters.remove(name); + } else { + this.mQueryParameters.put(name, value); + } + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + Iterator iterator = this.mQueryParameters.keySet().iterator(); + while (iterator.hasNext()) { + String s = (String) iterator.next(); + builder.append(s, this.mQueryParameters.get(s)); + } + return builder.toString(); + } + + /** + * any request with version-specific handling should deal with this. + * + * @param version + */ + public abstract void setVersion(RetsVersion version); + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/RetsHttpResponse.java b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsHttpResponse.java new file mode 100644 index 0000000..315464e --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsHttpResponse.java @@ -0,0 +1,26 @@ +package com.ossez.usreio.client; + +import java.io.InputStream; +import java.util.Map; + +/** + * Interface for retrieving useful header fields from a RETS HTTP response + * + * @author YuCheng Hu + */ +public interface RetsHttpResponse { + public int getResponseCode() throws RetsException; + + public Map getHeaders() throws RetsException; + + public String getHeader(String hdr) throws RetsException; + + public String getCookie(String cookie) throws RetsException; + + public String getCharset() throws RetsException; + + public InputStream getInputStream() throws RetsException; + + public Map getCookies() throws RetsException; + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/RetsSession.java b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsSession.java new file mode 100644 index 0000000..6e91e1e --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsSession.java @@ -0,0 +1,403 @@ +package com.ossez.usreio.client; + +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.Metadata; +import com.ossez.usreio.tests.common.metadata.MetadataException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * RetsSession is the core class of the rets.client package. + */ +public class RetsSession { + public static final String METADATA_TABLES = "metadata_tables.xml"; + public static final String RETS_CLIENT_VERSION = "1.5";//change default version + + private static final Log LOG = LogFactory.getLog(RetsSession.class); + private static String sUserAgent = "crt-rets-client/" + RETS_CLIENT_VERSION; + + private CapabilityUrls capabilityUrls; + private RetsHttpClient httpClient; + private RetsTransport transport; + private String sessionId; + + + /** + * Creates a new RetsSession instance. + * You must call login(user, pass) before attempting any other + * transactions. + * + * Uses a default implementation of RetsHttpClient based on + * apache commons http client. + * + * Uses the RetsVersion.RETS_DEFAULT as the RetsVersion for + * this session. + * + * Uses sAgent at the User-Agent setting for this RetsSession. + * + * @param loginUrl URL of the Login transaction. + */ + public RetsSession(String loginUrl) { + this(loginUrl, new CommonsHttpClient()); + } + + /** + * Creates a new RetsSession instance. + * You must call login(user, pass) before attempting any other + * transactions. + * + * Uses the RetsVersion.RETS_DEFAULT as the RetsVersion for + * this session. + * + * Uses sAgent at the User-Agent setting for this RetsSession. + * + * @param loginUrl URL of the Login transaction + * @param httpClient a RetsHttpClient implementation. The default + * is CommonsHttpClient. + */ + public RetsSession(String loginUrl, RetsHttpClient httpClient) { + this(loginUrl, httpClient, RetsVersion.DEFAULT); + } + + /** + * Creates a new RetsSession instance. + * You must call login(user, pass) before attempting any other + * transactions. + * + * Uses sAgent at the User-Agent setting for this RetsSession. + * + * @param loginUrl URL of the Login transaction + * @param httpClient a RetsHttpClient implementation. The default + * is CommonsHttpClient. + * @param retsVersion The RetsVersion used by this RetsSession. + */ + public RetsSession(String loginUrl, RetsHttpClient httpClient, RetsVersion retsVersion) { + this(loginUrl, httpClient, retsVersion, sUserAgent,false); + } + + /** + * Creates a new RetsSession instance. + * You must call login(user, pass) before attempting any other + * transactions. + * + * @param loginUrl URL of the Login transaction + * @param httpClient a RetsHttpClient implementation. The default + * is CommonsHttpClient. + * @param retsVersion The RetsVersion used by this RetsSession. + * @param userAgent specific User-Agent to use for this session. + */ + public RetsSession(String loginUrl, RetsHttpClient httpClient, RetsVersion retsVersion, String userAgent, boolean strict) { + this.capabilityUrls = new CapabilityUrls(); + this.capabilityUrls.setLoginUrl(loginUrl); + + this.httpClient = httpClient; + this.transport = new RetsTransport(httpClient, this.capabilityUrls, retsVersion, strict); + this.httpClient.addDefaultHeader("User-Agent", userAgent); + } + + /** + * Query the current RetsVersion being used in this session. + * + * Initially, this will be the value passed to the RetsTransport. + * However, if during auto-negotiation the RetsTransport changes + * the RetsSession, this value may change throughout the session. + * + * @return the current RetsVersion value being used by the + * RetsTransport. + */ + public RetsVersion getRetsVersion() { + return this.transport.getRetsVersion(); + } + + /** + * Get the current RETS Session ID + * + * @return the current RETS Session ID or null is the server has + * not specified one + */ + public String getSessionId() { + return this.sessionId; + } + + public void setSessionId(String sessionId) { + LOG.debug("setting Session-ID to: " + sessionId); + this.sessionId = sessionId; + } + + public void setMonitor(NetworkEventMonitor monitor) { + this.transport.setMonitor(monitor); + } + + public void setStrict(boolean strict) { + this.transport.setStrict(strict); + } + public boolean isStrict() { + return this.transport.isStrict(); + } + + /** + * Sets the default User-Agent value for RetsSessions created without + * a specified User-Agent value. + * + * @param userAgent Default User-Agent value to use for all RetsSession + * objects created in the future. + */ + public static void setUserAgent(String userAgent) { + sUserAgent = userAgent; + } + + public String getLoginUrl() { + return this.capabilityUrls.getLoginUrl(); + } + + public Metadata getIncrementalMetadata() throws RetsException { + try { + return new Metadata(new MetaCollectorImpl(this.transport)); + } catch (MetadataException e) { + throw new RetsException(e); + } + } + + /** + * Get the complete RETS metadata. + * + * @return The RETS metadata object for these credentials. + * + * @throws RetsException + */ + public Metadata getMetadata() throws RetsException { + return this.transport.getMetadata("null"); + } + /** + * Ability to download the raw metadata to a location + * @param location + * @return + * @throws RetsException + */ + public Metadata getMetadata(String location) throws RetsException { + return this.transport.getMetadata(location); + } + + /** + * Perform a low level GetMetadatRequest. To retrieve + * structured metadata, + * + * @see #getMetadata() + * + * @param req GetMetadataRequest + * @return GetMetadataResponse, containing all MetaObjects + * returned + * + * @throws RetsException if an error occurs + */ + public GetMetadataResponse getMetadata(GetMetadataRequest req) throws RetsException { + return this.transport.getMetadata(req); + } + + /** + * Fetches the action (MOTD) from the server. + * + * @exception RetsException if an error occurs + */ + private void getAction() throws RetsException { + String actionUrl = this.capabilityUrls.getActionUrl(); + if (actionUrl == null) { + LOG.warn("No Action-URL available, skipping"); + return; + } + GenericHttpRequest actionRequest = new GenericHttpRequest(actionUrl){ + @Override + public Map getHeaders() { + return null; + } + }; + RetsHttpResponse httpResponse = this.httpClient.doRequest("GET", actionRequest); + try { + httpResponse.getInputStream().close(); + } catch (Exception e) { + LOG.error("Action URL weirdness", e); + } + } + + /** + * Implementation that allow for single or multi-part + * GetObject requests. + * + * @param req + * @return + * @exception RetsException if an error occurs + */ + public GetObjectResponse getObject(GetObjectRequest req) throws RetsException { + return this.transport.getObject(req); + } + + /** + * + * @param resource + * @param type + * @param entity + * @param id + * @return response + * @exception RetsException if an error occurs + */ + public GetObjectResponse getObject(String resource, String type, String entity, String id) throws RetsException { + GetObjectRequest req = new GetObjectRequest(resource, type); + req.addObject(entity, id); + return getObject(req); + } + + /** + * Log into the RETS server (see RETS 1.5, section 4). No other + * transactions will work until you have logged in. + * + * @param userName Username to authenticate + * @param password Password to authenticate with + * @return LoginResponse if success. + * @exception RetsException if authentication was denied + */ + public LoginResponse login(String userName, String password) throws RetsException { + return login(userName, password, null, null); + } + + /** + * Log into the RETS server (see RETS 1.5, section 4). No other + * transactions will work until you have logged in. + * + * @param userName username to authenticate + * @param password password to authenticate with + * @param brokerCode broker code if the same user belongs to multiple + * brokerages. May be null. + * @param brokerBranch branch code if the same user belongs to multiple + * branches. May be null. brokerCode is required if you want + * brokerBranch to work. + * @return LoginResponse if success. + * @exception RetsException if authentication was denied + */ + + public LoginResponse login(String userName, String password, String brokerCode, String brokerBranch) throws RetsException { + this.httpClient.setUserCredentials(userName, password); + + LoginRequest request = new LoginRequest(); + request.setBrokerCode(brokerCode, brokerBranch); + + LoginResponse response = this.transport.login(request); + this.capabilityUrls = response.getCapabilityUrls(); + this.transport.setCapabilities(this.capabilityUrls); + this.setSessionId(response.getSessionId()); + this.getAction(); + + return response; + } + + /** + * Log out of the current session. Another login _may_ re-establish a new connection + * depending the the behavior of the {#link RetsHttpClient} and its' ability to + * maintain and restablish a connection. + * + * @return a LogoutResponse + * @throws RetsException if the logout transaction failed + */ + public LogoutResponse logout() throws RetsException { + try { + return this.transport.logout(); + } finally { + this.setSessionId(null); + } + } + + /** + * Will perform a search as requested and return a filled + * SearchResult object. This method caches all result information + * in memory in the SearchResult object. + * + * @param req Contains parameters on which to search. + * @return a completed SearchResult + * @exception RetsException if an error occurs + */ + public SearchResult search(SearchRequest req) throws RetsException { + SearchResultImpl res = new SearchResultImpl(); + search(req, res); + return res; + } + + /** + * Execute a RETS Search. The collector object will be filled + * when this method is returned. See RETS 1.52d, Section 5. + * + * @param req Contains parameters on which to search. + * @param collector SearchResult object which will be informed of the results + * as they come in. If you don't need live results, see the other + * search invocation. + * @exception RetsException if an error occurs + */ + public void search(SearchRequest req, SearchResultCollector collector) throws RetsException { + this.transport.search(req, collector); + } + + /** + * Search and process the Search using a given SearchResultProcessor. + * + * @param req the search request + * @param processor the result object that will process the data + */ + public SearchResultSet search(SearchRequest req, SearchResultProcessor processor) throws RetsException { + return this.transport.search(req, processor); + } + + /** + * The lowest level integration. This method is not recommened for general use. + */ + public RetsHttpResponse request(RetsHttpRequest request) throws RetsException{ + return this.transport.doRequest(request); + } + + /** + * switch to a specific HttpMethodName, POST/GET, where the + * method is supported. Where GET is not supported, POST + * will be used. + * @param method the HttpMethodName to use + */ + public void setMethod(String method) { + this.transport.setMethod(method); + } + + /** Make sure GC'd sessions are logged out. */ + @Override + protected void finalize() throws Throwable { + try { + if( this.sessionId != null ) this.logout(); + } finally { + super.finalize(); + } + } + /** + * Performs a search returning only the number of records resulting from a query. + * + * Convenience method to get number records from a query + * + * @param req the search request + * @return the number of records that returned from the search request + * @throws RetsException + */ + public int getQueryCount(SearchRequest req) throws RetsException { + req.setCountOnly(); + SearchResult res = this.search(req); + return res.getCount(); + } + + /** + * Gives the URL's of an Object request instead of object themselves + * + * Convenience method to get the URL's of the requeseted object only + * + * @param req + * @return + * @throws RetsException + */ + public GetObjectResponse getObjectUrl(GetObjectRequest req) throws RetsException { + req.setLocationOnly(true); + GetObjectResponse res = this.getObject(req); + return res; + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/RetsTransport.java b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsTransport.java new file mode 100644 index 0000000..dbe9775 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsTransport.java @@ -0,0 +1,334 @@ +package com.ossez.usreio.client; + + +import java.io.FileWriter; +import java.util.HashMap; +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.JDomCompactBuilder; +import com.ossez.usreio.tests.common.metadata.JDomStandardBuilder; +import com.ossez.usreio.tests.common.metadata.Metadata; +import com.ossez.usreio.tests.common.metadata.MetadataBuilder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.dom4j.Document; +import org.dom4j.io.SAXReader; +import org.dom4j.io.XMLWriter; + + +/** + * Implements the basic transport mechanism. This class deals with the + * very basic parts of sending the request, returning a response object, + * and version negotiation. + * + */ +public class RetsTransport { + private static final String RETS_SESSION_ID_HEADER = "RETS-Session-ID"; // TODO spec says hyphen, Marketlinx uses an underscore + + private RetsHttpClient client; + private CapabilityUrls capabilities; + private String method = "GET"; + private RetsVersion version; + private boolean strict; + private NetworkEventMonitor monitor; + + private static final Log LOG = LogFactory.getLog(RetsTransport.class); + + private static Map MONITOR_MSGS = new HashMap(){{ + put(ChangePasswordRequest.class, "Transmitting change password request"); + put(GetObjectRequest.class, "Retrieving media object"); + put(LoginRequest.class, "Logging in"); + put(GetMetadataRequest.class, "Retrieving metadata"); + put(LogoutRequest.class, "Logging out"); + put(SearchRequest.class, "Executing search"); + }}; + + + /** + * Create a new transport instance. + * @param client An http client (make sure you call setUserCredentials + * on it before carrying out any transactions). + * @param capabilities the initial capabilities url list. This can be + * replaced with a more up to date version at any time (for example, + * post-login()) with setCapabilities() + * + * @see RetsHttpClient#setUserCredentials + */ + public RetsTransport(RetsHttpClient client, CapabilityUrls capabilities) { + this(client, capabilities, RetsVersion.DEFAULT, false); + } + + /** + * Create a new transport instance to speak a specific RETS version. + * @param client an http client + * @param capabilities the initial capabilities url list + * @param version the RETS version to use during initial negotiation + * (RetsTransport will automatically switch to whatever version the + * server supports). + */ + public RetsTransport(RetsHttpClient client, CapabilityUrls capabilities, RetsVersion version, boolean strict) { + this.client = client; + this.capabilities = capabilities; + this.doVersionHeader(version); + this.strict = strict; + this.client.addDefaultHeader("Accept", "*/*"); + this.monitor = new NullNetworkEventMonitor(); + } + + /** + * Query the current RetsVersion being used in this RetsTransport. + * + * Initially, this will be the value with which this object was + * constructed. + * + * However, this value may change after login. + * + * @return the current RetsVersion value being used by the + * RetsTransport. + */ + public RetsVersion getRetsVersion() { + return this.version; + } + + public boolean isStrict() { + return this.strict; + } + + public void setStrict(boolean strict) { + this.strict = strict; + } + + public void setMonitor(NetworkEventMonitor monitor) { + if (monitor == null) { + monitor = new NullNetworkEventMonitor(); + } + this.monitor = monitor; + } + + /** + * Set our RetsHttpClient up with the correct default RETS version to use, + * default to RETS 1.5. + * @param retsVersion + */ + private void doVersionHeader(RetsVersion retsVersion) { + if (this.client == null) + return; + if (retsVersion == null) + retsVersion = RetsVersion.DEFAULT; + this.version = retsVersion; + this.client.addDefaultHeader(RetsVersion.RETS_VERSION_HEADER, this.version.toString()); + } + + /** + * replace the capabilities url list with a new one + * @param capabilities the new capabilities url list + */ + public void setCapabilities(CapabilityUrls capabilities) { + this.capabilities = capabilities; + } + + /** + * switch to a specific HttpMethodName, POST/GET, where the + * method is supported. Where GET is not supported, POST + * will be used. + * @param method the HttpMethodName to use + */ + public void setMethod(String method) { + this.method = method; + } + + /** + * Available as an integration last resort + */ + public RetsHttpResponse doRequest(RetsHttpRequest req) throws RetsException { + Object monitorobj = null; + String msg = getMonitorMessage(req); + monitorobj = this.monitor.eventStart(msg); + + req.setVersion(this.version); + req.setUrl(this.capabilities); + + RetsHttpResponse httpResponse; + try { + httpResponse = this.client.doRequest(this.method, req); + } finally { + this.monitor.eventFinish(monitorobj); + } + return httpResponse; + } + + private String getMonitorMessage(RetsHttpRequest req) { + String msg = (String) MONITOR_MSGS.get(req.getClass()); + if (msg == null) { + msg = "communicating with network"; + } + return msg; + } + + /** + * Logs into the server. This transaction gets a list of capability URLs + * encapsulated in the LoginResponse that should typically be given back + * to the transport object with setCapabilities(). RETS Specification, + * section 4. + * + * @param req The login request + * @return the LoginResponse object + * @throws RetsException if the login failed or something went wrong on the + * network + * @see #setCapabilities + */ + public LoginResponse login(LoginRequest req) throws RetsException { + RetsHttpResponse retsHttpResponse = this.doRequest(req); + + String versionHeader = retsHttpResponse.getHeader(RetsVersion.RETS_VERSION_HEADER); + // may be null, which is fine, return null, dont throw + RetsVersion retsVersion = RetsVersion.getVersion(versionHeader); + if( retsVersion == null && this.strict ) + throw new RetsException(String.format("RETS Version is a required response header, version '%s' is unrecognized",versionHeader)); + // skip updating the client version if its not set (correctly) by the server + if( retsVersion != null ) this.doVersionHeader(retsVersion); + + LoginResponse response = new LoginResponse(this.capabilities.getLoginUrl()); + + String sessionId = retsHttpResponse.getCookie(RETS_SESSION_ID_HEADER); + response.setSessionId(sessionId); + response.setStrict(this.strict); + response.parse(retsHttpResponse.getInputStream(), this.version); + return response; + } + + /** + * Logs out of the server. No other transactions should be called until + * another login() succeeds. RETS Specification, Section 6. Logout is + * an optional transaction. This method returns null if the server does + * not support the Logout transaction. + * + * @return LogoutResponse or null if logout is not supported + * @throws RetsException if there is a network or remote server error + */ + public LogoutResponse logout() throws RetsException { + if (this.capabilities.getLogoutUrl() == null) { + return null; + } + RetsHttpRequest req = new LogoutRequest(); + RetsHttpResponse httpResponse = doRequest(req); + LogoutResponse response = new LogoutResponse(); + response.setStrict(this.strict); + try { + response.parse(httpResponse.getInputStream(), this.version); + } catch(RetsException e) { + if (e.getMessage().contains("Invalid number of children")){// most RETS servers have issues logging out for some reason. + LOG.warn("unsual response for logout request, but log out successful."); + } + + } + return response; + } + + /** + * Perform a non-streaming search and pass all results from the + * SearchRequest to the given collector. + * + * 12/06/20 Added charset, needed for sax parser + * @param req the search request + * @param collector the result object that will store the data + */ + public void search(SearchRequest req, SearchResultCollector collector) throws RetsException { + RetsHttpResponse httpResponse = doRequest(req); + new SearchResultHandler(collector).parse(httpResponse.getInputStream(), httpResponse.getCharset()); + } + + /** + * Override processing of the search completely by providing a + * SearchResultProcessor to process the results of the Search. + * + * @param req the search request + * @param processor the result object that will process the data + */ + public SearchResultSet search(SearchRequest req, SearchResultProcessor processor) throws RetsException { + RetsHttpResponse httpResponse = doRequest(req); + return processor.parse(httpResponse.getInputStream()); + } + + /** + * + * @param req GetObject request + * @return a GetObjectResponse + * @throws RetsException if the request is not valid or a network error + * occurs + */ + public GetObjectResponse getObject(GetObjectRequest req) throws RetsException { + if (this.capabilities.getGetObjectUrl() == null) { + throw new RetsException("Server does not support GetObject transaction."); + } + req.setUrl(this.capabilities); + RetsHttpResponse httpResponse = this.client.doRequest(this.method, req); + GetObjectResponse result = new GetObjectResponse(httpResponse.getHeaders(), httpResponse.getInputStream()); + return result; + } + + public Metadata getMetadata(String location) throws RetsException { + boolean compact = Boolean.getBoolean("rets-client.metadata.compact"); + GetMetadataRequest req = new GetMetadataRequest("SYSTEM", "*"); + if (compact) { + req.setCompactFormat(); + } + try { + RetsHttpResponse httpResponse = doRequest(req); + Object monitorobj = null; + monitorobj = this.monitor.eventStart("Parsing metadata"); + try { + SAXReader xmlBuilder = new SAXReader(); + Document xmlDocument = xmlBuilder.read(httpResponse.getInputStream()); + if (!location.equals("null")){ + + FileWriter writer = new FileWriter(location); + XMLWriter outputter = new XMLWriter(writer); + + outputter.write(xmlDocument); + outputter.close(); + + } + MetadataBuilder metadataBuilder; + if (req.isCompactFormat()) { + metadataBuilder = new JDomCompactBuilder(); + } else { + metadataBuilder = new JDomStandardBuilder(); + } + metadataBuilder.setStrict(this.strict); + + + + return metadataBuilder.doBuild(xmlDocument); + } finally { + this.monitor.eventFinish(monitorobj); + } + } catch (Exception e) { + throw new RetsException(e); + } + } + + public GetMetadataResponse getMetadata(GetMetadataRequest req) throws RetsException { + RetsHttpResponse httpResponse = doRequest(req); + Object monitorobj = null; + monitorobj = this.monitor.eventStart("Parsing metadata"); + try { + try { + return new GetMetadataResponse(httpResponse.getInputStream(), req.isCompactFormat(),this.strict); + } catch (InvalidReplyCodeException e) { + e.setRequestInfo(req.toString()); + throw e; + } + } finally { + this.monitor.eventFinish(monitorobj); + } + } + + public boolean changePassword(ChangePasswordRequest req) throws RetsException { + RetsHttpResponse httpResponse = doRequest(req); + ChangePasswordResponse response = new ChangePasswordResponse(httpResponse.getInputStream()); + // response will throw an exception if there is an error code + return (response != null); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/RetsUtil.java b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsUtil.java new file mode 100644 index 0000000..7eb6787 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsUtil.java @@ -0,0 +1,48 @@ +package com.ossez.usreio.client; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.net.URLCodec; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Random utility functions + * + * @author YuCheng Hu + */ +public class RetsUtil { + public static void copyStream(InputStream in, OutputStream out) throws IOException { + byte[] buf = new byte[512]; + int count; + while (true) { + count = in.read(buf); + if (count < 1) { + in.close(); + out.close(); + return; + } + while (count > 0) { + out.write(buf); + } + } + } + + public static String urlEncode(String string) { + try { + return new URLCodec().encode(string); + } catch (EncoderException e) { + throw new RuntimeException(e); + } + } + + public static String urlDecode(String string) { + try { + return new URLCodec().decode(string); + } catch (DecoderException e) { + throw new RuntimeException(e); + } + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/RetsVersion.java b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsVersion.java new file mode 100644 index 0000000..e7f7fdf --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/RetsVersion.java @@ -0,0 +1,106 @@ +package com.ossez.usreio.client; + +import java.io.Serializable; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; + +/** + * RetsVersion + * + * @author YuCheng Hu + */ +public class RetsVersion implements Serializable { + + public static final String RETS_VERSION_HEADER = "RETS-Version"; + + public static final RetsVersion RETS_10 = new RetsVersion(1, 0, 0, 0); + public static final RetsVersion RETS_15 = new RetsVersion(1, 5, 0, 0); + public static final RetsVersion RETS_16 = new RetsVersion(1, 6, 0, 0); + public static final RetsVersion RETS_17 = new RetsVersion(1, 7, 0, 0); + public static final RetsVersion RETS_1_7_2 = new RetsVersion(1, 7, 2, 0); + public static final RetsVersion RETS_1_8 = new RetsVersion(1, 8, 0, 0); + public static final RetsVersion RETS_1_9 = new RetsVersion(1, 9, 0, 0); + public static final RetsVersion DEFAULT = RETS_1_7_2; + + private int mMajor; + private int mMinor; + private int mRevision; + private int mDraft; + + public RetsVersion(int major, int minor) { + this(major, minor, 0, 0); + } + + /** + * @deprecated use new RetsVersion(major, minor, 0, draft) + */ + @Deprecated + public RetsVersion(int major, int minor, int draft) { + this(major, minor, 0, draft); + } + + public RetsVersion(int major, int minor, int revision, int draft) { + this.mMajor = major; + this.mMinor = minor; + this.mRevision = revision; + this.mDraft = draft; + } + + public int getMajor() { + return this.mMajor; + } + + public int getMinor() { + return this.mMinor; + } + + public int getRevision() { + return this.mRevision; + } + + public int getDraft() { + return this.mDraft; + } + + @Override + public String toString() { + if (this.mRevision == 0) { + if (this.mDraft == 0) { + return "RETS/" + this.mMajor + "." + this.mMinor; + } + return "RETS/" + this.mMajor + "." + this.mMinor + "d" + this.mDraft; + } + if (this.mDraft == 0) { + return "RETS/" + this.mMajor + "." + this.mMinor + "." + this.mRevision; + } + return "RETS/" + this.mMajor + "." + this.mMinor + "." + this.mRevision + "d" + this.mDraft; + } + + @Override + public boolean equals(Object o) { + if (o instanceof RetsVersion) { + RetsVersion v = (RetsVersion) o; + if ((v.getMajor() == this.mMajor) && (v.getMinor() == this.mMinor) && (v.getRevision() == this.mRevision) && (v.getDraft() == this.mDraft)) { + return true; + } + } + return false; + } + + public static RetsVersion getVersion(String ver) { + if (StringUtils.isEmpty(ver)) return null; + String[] split = StringUtils.trimToEmpty(ver).split("\\."); + int ma = NumberUtils.toInt(split[0], 1); + int mn = split.length > 1 ? NumberUtils.toInt(split[1], 0) : 0; + int re = 0; + int dr = 0; + if (split.length > 2) { + split = StringUtils.defaultString(split[2]).split("d"); + re = NumberUtils.toInt(split[0], 0); + dr = split.length > 1 ? NumberUtils.toInt(split[1], 0) : 0; + } + return new RetsVersion(ma, mn, re, dr); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/SearchRequest.java b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchRequest.java new file mode 100644 index 0000000..9cbc3fa --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchRequest.java @@ -0,0 +1,122 @@ +package com.ossez.usreio.client; + +/** + * + * The search request sent from search() in RetsSession + * + */ + +public class SearchRequest extends RetsHttpRequest { + + public static final int COUNT_NONE = 1; + public static final int COUNT_FIRST = 2; + public static final int COUNT_ONLY = 3; + public static final String FORMAT_STANDARD_XML = "STANDARD-XML"; + public static final String FORMAT_COMPACT = "COMPACT"; + public static final String FORMAT_COMPACT_DECODED = "COMPACT-DECODED"; + public static final String RETS_DMQL1 = "DMQL"; + public static final String RETS_DMQL2 = "DMQL2"; + public static final String KEY_TYPE = "SearchType"; + public static final String KEY_CLASS = "Class"; + public static final String KEY_DMQLVERSION = "QueryType"; + public static final String KEY_QUERY = "Query"; + public static final String KEY_COUNT = "Count"; + public static final String KEY_FORMAT = "Format"; + public static final String KEY_LIMIT = "Limit"; + public static final String KEY_OFFSET = "Offset"; + public static final String KEY_SELECT = "Select"; + public static final String KEY_RESTRICTEDINDICATOR = "RestrictedIndicator"; + public static final String KEY_STANDARDNAMES = "StandardNames"; + + + private String type; + + public SearchRequest(String stype, String sclass, String query) { + setQueryParameter(KEY_TYPE, stype); + this.type = stype; + setQueryParameter(KEY_CLASS, sclass); + setQueryParameter(KEY_QUERY, query); + setQueryParameter(KEY_FORMAT, FORMAT_COMPACT); + setQueryParameter(KEY_DMQLVERSION, RETS_DMQL2); + } + + + @Override + public void setUrl(CapabilityUrls urls) { + setUrl(urls.getSearchUrl()); + } + + public String getType() { + return this.type; + } + + public void setCountNone() { + setQueryParameter(KEY_COUNT, null); + } + + public void setCountFirst() { + setQueryParameter(KEY_COUNT, "1"); + } + + public void setCountOnly() { + setQueryParameter(KEY_COUNT, "2"); + } + + public void setFormatCompact() { + setQueryParameter(KEY_FORMAT, FORMAT_COMPACT); + } + + public void setFormatCompactDecoded() { + setQueryParameter(KEY_FORMAT, FORMAT_COMPACT_DECODED); + } + + public void setFormatStandardXml() { + setQueryParameter(KEY_FORMAT, FORMAT_STANDARD_XML); + } + + public void setFormatStandardXml(String dtdVersion) { + setQueryParameter(KEY_FORMAT, FORMAT_STANDARD_XML + ":" + dtdVersion); + } + + public void setLimit(int count) { + setQueryParameter(KEY_LIMIT, Integer.toString(count)); + } + + public void setLimitNone() { + setQueryParameter(KEY_LIMIT, null); + } + + public void setSelect(String sel) { + setQueryParameter(KEY_SELECT, sel); + } + + public void setRestrictedIndicator(String rest) { + setQueryParameter(KEY_RESTRICTEDINDICATOR, rest); + } + + public void setStandardNames() { + setQueryParameter(KEY_STANDARDNAMES, "1"); + } + + public void setSystemNames() { + setQueryParameter(KEY_STANDARDNAMES, null); + } + + public void setOffset(int offset) { + setQueryParameter(KEY_OFFSET, Integer.toString(offset)); + } + + public void setOffsetNone() { + setQueryParameter(KEY_OFFSET, null); + } + + /** TODO should the search automatically handle this??? shouldn't this be setable by vendor is that predicatable? */ + @Override + public void setVersion(RetsVersion ver) { + if (RetsVersion.RETS_10.equals(ver)) { + setQueryParameter(KEY_DMQLVERSION, RETS_DMQL1); + } else { + setQueryParameter(KEY_DMQLVERSION, RETS_DMQL2); + } + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResult.java b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResult.java new file mode 100644 index 0000000..73dfc79 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResult.java @@ -0,0 +1,23 @@ +package com.ossez.usreio.client; + +import java.util.NoSuchElementException; +import java.util.Iterator; + +/** + * Interface for retrieving additional information from of a result from a RETS query/search + * + */ + +public interface SearchResult extends SearchResultInfo { + public String[] getRow(int idx) throws NoSuchElementException; + + public Iterator iterator(); + + public String[] getColumns(); + + public boolean isMaxrows(); + + public int getCount(); + + public boolean isComplete(); +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultCollector.java b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultCollector.java new file mode 100644 index 0000000..7e346b6 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultCollector.java @@ -0,0 +1,18 @@ +package com.ossez.usreio.client; + +/** + * Interface for a setting properties of a result from a query (used by SearchResultHandler) + */ + +public interface SearchResultCollector { + + public void setCount(int count); + + public void setColumns(String[] columns); + + public boolean addRow(String[] row); + + public void setMaxrows(); + + public void setComplete(); +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultHandler.java b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultHandler.java new file mode 100644 index 0000000..b0003ed --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultHandler.java @@ -0,0 +1,280 @@ +package com.ossez.usreio.client; + +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; +import java.util.StringTokenizer; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.XMLReader; +/** + * + * Handles XML parsing from response setting the proper fields using a SearchResultCollector + * + */ +public class SearchResultHandler implements ContentHandler, ErrorHandler{ + private static final Log LOG = LogFactory.getLog(SearchResultHandler.class); + private static SAXParserFactory FACTORY = SAXParserFactory.newInstance(); + + private int dataCount; + private SearchResultCollector collector; + private StringBuffer currentEntry; + private String delimiter; + private Locator locator; + private String[] columns; + private InvalidReplyCodeHandler invalidReplyCodeHandler; + private CompactRowPolicy compactRowPolicy; + + public SearchResultHandler(SearchResultCollector r) { + this(r, InvalidReplyCodeHandler.FAIL, CompactRowPolicy.DEFAULT); + } + + public SearchResultHandler(SearchResultCollector r, InvalidReplyCodeHandler invalidReplyCodeHandler, CompactRowPolicy badRowPolicy) { + this.compactRowPolicy = badRowPolicy; + if (r == null) + throw new NullPointerException("SearchResultCollector must not be null"); + + if (invalidReplyCodeHandler == null) + throw new NullPointerException("InvalidReplyCodeHandler must not be null"); + + if (badRowPolicy == null) + throw new NullPointerException("BadRowPolicy must not be null"); + + this.collector = r; + this.dataCount = 0; + this.invalidReplyCodeHandler = invalidReplyCodeHandler; + } + + public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { + String name = localName; + if (localName.equals("")) { + name = qName; + } + if (name.equals("RETS") || name.equals("RETS-STATUS")) { + String rawrepcode = atts.getValue("ReplyCode"); + try { + int repcode = Integer.parseInt(rawrepcode); + if (repcode > 0) { + try { + if (ReplyCode.MAXIMUM_RECORDS_EXCEEDED.equals(repcode)) + return; + + if (ReplyCode.NO_RECORDS_FOUND.equals(repcode)) + return; + + if (name.equals("RETS")) + this.invalidReplyCodeHandler.invalidRetsReplyCode(repcode); + else + this.invalidReplyCodeHandler.invalidRetsStatusReplyCode(repcode); + } catch (InvalidReplyCodeException e) { + String text = atts.getValue("", "ReplyText"); + e.setRemoteMessage(text); + throw new SAXException(e); + } + } + } catch (NumberFormatException e) { + throw new SAXParseException("Invalid ReplyCode '" + rawrepcode + "'", this.locator); + } + return; + } + if (name == "COUNT") { + String s = atts.getValue("Records"); + if (s == null) { + s = atts.getValue("", "Records"); + if (s == null) { + throw new SAXParseException("COUNT tag has no Records " + "attribute", this.locator); + } + } + int i = Integer.parseInt(s, 10); + this.collector.setCount(i); + return; + } + if (name == "DELIMITER") { + String s = atts.getValue("value"); + if (s == null) { + s = atts.getValue("", "value"); + if (s == null) { + throw new RuntimeException("Invalid Delimiter"); + } + } + int i = Integer.parseInt(s, 16); + this.delimiter = "" + (char) i; + return; + } + if (name == "COLUMNS" || name == "DATA") { + this.currentEntry = new StringBuffer(); + return; + } + if (name == "MAXROWS") { + this.collector.setMaxrows(); + return; + } + // Unknown tag. danger, will. + LOG.warn("Unknown tag: " + name + ", qName = " + qName); + + } + + public void characters(char[] ch, int start, int length) { + if (this.currentEntry != null) { + this.currentEntry.append(ch, start, length); + } + } + + public void ignorableWhitespace(char[] ch, int start, int length) { + // we ignore NOZINK! + characters(ch, start, length); + } + + /** do NOT use string.split() unless your prepared to deal with loss due to token boundary conditions */ + private String[] split(String input) throws SAXParseException { + if (this.delimiter == null) { + throw new SAXParseException("Invalid compact format - DELIMITER not specified", this.locator); + } + if( !input.startsWith(this.delimiter) ){ + throw new SAXParseException("Invalid compact format", this.locator); + } + StringTokenizer tkn = new StringTokenizer(input, this.delimiter, true); + List list = new LinkedList(); + tkn.nextToken(); // junk the first element + String last = null; + while (tkn.hasMoreTokens()) { + String next = tkn.nextToken(); + if (next.equals(this.delimiter)) { + if (last == null) { + list.add(""); + } else { + last = null; + } + } else { + list.add(next); + last = next; + } + } + return (String[]) list.toArray(new String[0]); + } + + public void endElement(String uri, String localName, String qName) throws SAXParseException { + String name = localName; + if (name.equals("")) { + name = qName; + } + if (name.equals("COLUMNS") || name.equals("DATA")) { + String[] contents = split(this.currentEntry.toString()); + if (name.equals("COLUMNS")) { + this.collector.setColumns(contents); + this.columns = contents; + } else { + if( this.compactRowPolicy.apply(this.dataCount, this.columns, contents) ) { + this.dataCount++; + this.collector.addRow(contents); + } + } + this.currentEntry = null; + } + } + + public void startDocument() { + LOG.info("Start document"); + } + + public void endDocument() { + LOG.info("Document ended"); + this.collector.setComplete(); + } + + public void startPrefixMapping(String prefix, String uri) throws SAXException { + // LOG.debug("prefix mapping: " + prefix); + } + + public void endPrefixMapping(String prefix) throws SAXException { + // LOG.debug("prefix mapping: " + prefix); + } + + public void processingInstruction(String target, String data) throws SAXException { + throw new SAXException("processing instructions not supported: " + "target=" + target + ", data=" + data); + } + + public void skippedEntity(String name) throws SAXException { + throw new SAXException("skipped entities not supported: name=" + name); + } + + public void setDocumentLocator(Locator locator) { + this.locator = locator; + } + + public void error(SAXParseException e) throws SAXException { + throw e; + } + + public void fatalError(SAXParseException e) throws SAXException { + throw e; + } + + public void warning(SAXParseException e) { + LOG.warn("an error occured while parsing. Attempting to continue", e); + } + + + + public void parse(InputSource src) throws RetsException { + parse(src, null); + } + /** + * + * created in order to pass the charset to the parser for proper encoding + * @param str + * @param charset + * @throws RetsException + */ + + public void parse(InputStream str, String charset) throws RetsException { + parse(new InputSource(str), charset); + try { + str.close(); + } catch (IOException e) { + throw new RetsException(e); + } + } + /** + * Pareses given source with the given charset + * + * @param src + * @throws RetsException + */ + public void parse(InputSource src, String charset) throws RetsException { + String encoding = src.getEncoding(); + if (encoding == null && (charset != null)){ + encoding = charset; + LOG.warn("Charset from headers:" + charset + ". Setting as correct encoding for parsing"); + src.setEncoding(encoding); + } + try { + SAXParser p = FACTORY.newSAXParser(); + XMLReader r = p.getXMLReader(); + r.setContentHandler(this); + r.setErrorHandler(this); + r.parse(src); + } catch (SAXException se) { + if (se.getException() != null && se.getException() instanceof RetsException) { + throw (RetsException) se.getException(); + } + throw new RetsException(se); + } catch (Exception e) { + LOG.error("An exception occured", e); + throw new RetsException(e); + + } + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultImpl.java b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultImpl.java new file mode 100644 index 0000000..7fdf99b --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultImpl.java @@ -0,0 +1,87 @@ +package com.ossez.usreio.client; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.commons.logging.LogFactory; +/** + * Concrete Implementation of SearchResult interface + * + */ +public class SearchResultImpl implements SearchResult, SearchResultCollector { + + private String[] columnNames; + private int count; + private List rows; + private boolean maxRows; + private boolean complete; + + public SearchResultImpl() { + this.count = 0; + this.rows = new ArrayList(); + this.maxRows = false; + this.complete = false; + } + + public void setCount(int count) { + this.count = count; + } + + public int getCount() { + if (this.count > 0) { + return this.count; + } + return this.rows.size(); + } + + public int getRowCount() { + return this.rows.size(); + } + + public void setColumns(String[] columns) { + this.columnNames = columns; + } + + public String[] getColumns() { + return this.columnNames; + } + + public boolean addRow(String[] row) { + if (row.length > this.columnNames.length) { + throw new IllegalArgumentException(String.format("Invalid number of result columns: got %s, expected %s",row.length, this.columnNames.length)); + } + if (row.length < this.columnNames.length) { + LogFactory.getLog(SearchResultCollector.class).warn(String.format("Row %s: Invalid number of result columns: got %s, expected ",this.rows.size(), row.length, this.columnNames.length)); + } + return this.rows.add(row); + } + + public String[] getRow(int idx) { + if (idx >= this.rows.size()) { + throw new NoSuchElementException(); + } + return this.rows.get(idx); + } + + public Iterator iterator() { + return this.rows.iterator(); + } + + public void setMaxrows() { + this.maxRows = true; + } + + public boolean isMaxrows() { + return this.maxRows; + } + + public void setComplete() { + this.complete = true; + } + + public boolean isComplete() { + return this.complete; + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultInfo.java b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultInfo.java new file mode 100644 index 0000000..b6af74c --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultInfo.java @@ -0,0 +1,25 @@ +package com.ossez.usreio.client; + +/** + * Interface that describes high level information + * about the results of a search. + * @author jrayburn + */ +public interface SearchResultInfo { + public int getCount() throws RetsException; + + public String[] getColumns() throws RetsException; + + /** @throws IllegalStateException */ + public boolean isMaxrows() throws RetsException, IllegalStateException; + + /** + * Indicates that processing of this search + * is complete. + * + * @return true if this SearchResultSet is finished processing. + * @throws RetsException Thrown if there is an error + * processing the SearchResultSet. + */ + public boolean isComplete() throws RetsException; +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultProcessor.java b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultProcessor.java new file mode 100644 index 0000000..4614f6a --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultProcessor.java @@ -0,0 +1,13 @@ +package com.ossez.usreio.client; + +import java.io.InputStream; +import java.io.Reader; + +/** + * Interface for parsing results from a RETS query/search + */ +public interface SearchResultProcessor { + public SearchResultSet parse(InputStream in) throws RetsException; + + public SearchResultSet parse(Reader in) throws RetsException; +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultSet.java b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultSet.java new file mode 100644 index 0000000..ba2a4e5 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/SearchResultSet.java @@ -0,0 +1,15 @@ +package com.ossez.usreio.client; + +/** + * Iterator style interface for processing the results + * of a RETS search a single time. Information about the + * search can be retrieved once processing is complete by + * calling the getInfo() method. + * + * @author YuCheng Hu + */ +public interface SearchResultSet extends SearchResultInfo { + public String[] next() throws RetsException; + + public boolean hasNext() throws RetsException; +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/SingleObjectResponse.java b/rets-io-client/src/main/java/com/ossez/usreio/client/SingleObjectResponse.java new file mode 100644 index 0000000..7e07661 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/SingleObjectResponse.java @@ -0,0 +1,53 @@ +package com.ossez.usreio.client; + +import com.ossez.usreio.common.util.CaseInsensitiveTreeMap; + +import java.io.InputStream; +import java.util.Map; + +/** + * Representation of a single object returned + * from a RETS server. + * + * @author jrayburn + */ +public class SingleObjectResponse { + + public static final String CONTENT_TYPE = "Content-Type"; + public static final String LOCATION = "Location"; + public static final String CONTENT_DESCRIPTION = "Content-Description"; + public static final String OBJECT_ID = "Object-ID"; + public static final String CONTENT_ID = "Content-ID"; + + private Map headers; + private InputStream inputStream; + + public SingleObjectResponse(Map headers, InputStream in) { + this.headers = new CaseInsensitiveTreeMap(headers); + this.inputStream = in; + } + + public String getType() { + return (String) this.headers.get(CONTENT_TYPE); + } + + public String getContentID() { + return (String) this.headers.get(CONTENT_ID); + } + + public String getObjectID() { + return (String) this.headers.get(OBJECT_ID); + } + + public String getDescription() { + return (String) this.headers.get(CONTENT_DESCRIPTION); + } + + public String getLocation() { + return (String) this.headers.get(LOCATION); + } + + public InputStream getInputStream() { + return this.inputStream; + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/SinglePartInputStream.java b/rets-io-client/src/main/java/com/ossez/usreio/client/SinglePartInputStream.java new file mode 100644 index 0000000..b777a2e --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/SinglePartInputStream.java @@ -0,0 +1,64 @@ +package com.ossez.usreio.client; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.PushbackInputStream; + +class SinglePartInputStream extends FilterInputStream { + private static final int EOS = -1; + + private final String boundary; + private boolean eos; + + + SinglePartInputStream(PushbackInputStream partInput, String boundary) { + super(partInput); + this.boundary = boundary; + } + + @Override + public int read() throws IOException { + int read = this.getPushBackStream().read(); + // was this the start of a boundary? + if( read != '\r' && read != '\n' ) return read; + this.getPushBackStream().unread(read); + byte[] peek = new byte[ "\r\n".length() + this.boundary.length()]; + // if so, check and see if the rest of the boundary is next + int peekRead = this.getPushBackStream().read(peek); + this.getPushBackStream().unread(peek, 0, peekRead); + if( new String(peek).contains(this.boundary) ) return EOS; + // if not, just a coincidence, just return the byte + return this.getPushBackStream().read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if(this.eos) return EOS; + + int read = off; + for( ; read < off + len; read++) { + int nextByte = this.read(); + if(nextByte == EOS) { + this.eos = true; + break; + } + + b[read] = (byte) nextByte; + } + return ( read - off ); + } + + @Override + public int read(byte[] b) throws IOException { + return this.read(b, 0, b.length); + } + + @Override + public void close() { + // noop - part of a larger stream + } + + private PushbackInputStream getPushBackStream() { + return (PushbackInputStream) this.in; + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/StreamingSearchResultProcessor.java b/rets-io-client/src/main/java/com/ossez/usreio/client/StreamingSearchResultProcessor.java new file mode 100644 index 0000000..6bd92e8 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/StreamingSearchResultProcessor.java @@ -0,0 +1,324 @@ +package com.ossez.usreio.client; + +import java.io.InputStream; +import java.io.Reader; +import java.util.LinkedList; + +import org.apache.commons.logging.LogFactory; +import org.xml.sax.InputSource; + +/** + * SearchResultProcessor that returns a streaming SearchResult implementation. + * + * @author jrayburn + */ +public class StreamingSearchResultProcessor implements SearchResultProcessor { + private final int mBufferSize; + private final int mTimeout; + private InvalidReplyCodeHandler mInvalidReplyCodeHandler; + private CompactRowPolicy mCompactRowPolicy; + + /** + * Construct a StreamingSearchResultProcessor. + * + * Waits indefinitely for buffer to be read from by + * client. + * + * @param bufferSize + * How many rows to buffer + */ + public StreamingSearchResultProcessor(int bufferSize) { + this(bufferSize, 0); + } + + /** + * Construct a StreamingSearchResultProcessor. + * + * Waits timeout milliseconds for buffer to + * be read from by client. + * + * @param bufferSize + * How many rows to buffer + * + * @param timeout + * How long to wait, in milliseconds, for the buffer + * to be read from when full. 0 indicates an indefinite + * wait. + */ + public StreamingSearchResultProcessor(int bufferSize, int timeout) { + super(); + this.mBufferSize = bufferSize; + this.mTimeout = timeout; + } + + /** how to deal with badly delimited data */ + public void setCompactRowPolicy(CompactRowPolicy badRowPolicy) { + this.mCompactRowPolicy = badRowPolicy; + } + + private CompactRowPolicy getCompactRowPolicy() { + if (this.mCompactRowPolicy == null) + return CompactRowPolicy.DEFAULT; + return this.mCompactRowPolicy; + } + + public void setInvalidRelyCodeHandler(InvalidReplyCodeHandler invalidReplyCodeHandler) { + this.mInvalidReplyCodeHandler = invalidReplyCodeHandler; + } + + private InvalidReplyCodeHandler getInvalidRelyCodeHandler() { + if (this.mInvalidReplyCodeHandler == null) + return InvalidReplyCodeHandler.FAIL; + return this.mInvalidReplyCodeHandler; + } + + public SearchResultSet parse(InputStream reader) { + return parse(new InputSource(reader)); + } + + public SearchResultSet parse(Reader reader) { + return parse(new InputSource(reader)); + } + + public SearchResultSet parse(InputSource source) { + StreamingSearchResult result = new StreamingSearchResult(this.mBufferSize, this.mTimeout); + StreamingThread thread = new StreamingThread(source, result, this.getInvalidRelyCodeHandler(), this.getCompactRowPolicy()); + thread.start(); + return result; + } + +} + +class StreamingThread extends Thread { + private StreamingSearchResult mResult; + private InputSource mSource; + private InvalidReplyCodeHandler mInvalidReplyCodeHandler; + private CompactRowPolicy badRowPolicy; + + public StreamingThread(InputSource source, StreamingSearchResult result,InvalidReplyCodeHandler invalidReplyCodeHandler, CompactRowPolicy badRowPolicy) { + this.mSource = source; + this.mResult = result; + this.mInvalidReplyCodeHandler = invalidReplyCodeHandler; + this.badRowPolicy = badRowPolicy; + } + + @Override + public void run() { + SearchResultHandler handler = new SearchResultHandler(this.mResult, this.mInvalidReplyCodeHandler, this.badRowPolicy); + try { + handler.parse(this.mSource); + } catch (RetsException e) { + this.mResult.setException(e); + } catch (Exception e) { + // socket timeouts, etc while obtaining xml bytes from InputSource ... + this.mResult.setException(new RetsException("Low level exception while attempting to parse input from source.", e)); + } + } + +} + +class StreamingSearchResult implements SearchResultSet, SearchResultCollector { + + private static final int PREPROCESS = 0; + private static final int BUFFER_AVAILABLE = 1; + private static final int BUFFER_FULL = 2; + private static final int COMPLETE = 3; + + private final int timeout; + private final int bufferSize; + private final LinkedList buffer; + + private boolean mMaxrows; + private int state; + private String[] columns; + private int count; + private RetsException exception; + + public StreamingSearchResult(int bufferSize, int timeout) { + if (bufferSize < 1) + throw new IllegalArgumentException("[bufferSize=" + bufferSize + "] must be greater than zero"); + if (timeout < 0) + throw new IllegalArgumentException("[timeout=" + timeout + "] must be greater than or equal to zero"); + + this.bufferSize = bufferSize; + this.timeout = timeout; + this.state = PREPROCESS; + this.buffer = new LinkedList(); + this.count = -1; + this.columns = null; + this.exception = null; + } + + // ------------ Producer Methods + + public synchronized boolean addRow(String[] row) { + if (row.length > this.columns.length) { + throw new IllegalArgumentException(String.format("Invalid number of result columns: got %s, expected %s",row.length, this.columns.length)); + } + if (row.length < this.columns.length) { + LogFactory.getLog(SearchResultCollector.class).warn(String.format("Row %s: Invalid number of result columns: got %s, expected ",this.count, row.length, this.columns.length)); + } + + if (state() > BUFFER_FULL) { + if (this.exception == null) + setException(new RetsException("Attempting to add rows to buffer when in complete state")); + throw new RuntimeException(this.exception); + } + + // check complete. + while (checkRuntime() && state() == BUFFER_FULL) { + _wait(); + + if (state() >= BUFFER_FULL) { + if (this.exception == null) + setException(new RetsException("Timeout writing to streaming result set buffer, timeout length = " + + this.timeout)); + throw new RuntimeException(this.exception); + } + } + + this.buffer.addLast(row); + + if (this.bufferSize == this.buffer.size()) + pushState(BUFFER_FULL); + else + pushState(BUFFER_AVAILABLE); + + this.notifyAll(); + return true; + } + + public synchronized void setComplete() { + pushState(COMPLETE); + notifyAll(); + } + + public synchronized void setCount(int count) { + this.count = count; + pushState(PREPROCESS); + notifyAll(); + } + + public synchronized void setColumns(String[] columns) { + this.columns = columns; + pushState(BUFFER_AVAILABLE); + notifyAll(); + } + + public synchronized void setMaxrows() { + this.mMaxrows = true; + pushState(COMPLETE); + notifyAll(); + } + + synchronized void setException(RetsException e) { + this.exception = e; + pushState(COMPLETE); + notifyAll(); + } + + // ----------- Consumer Methods + + public synchronized boolean hasNext() throws RetsException { + // wait for someone to add data to the queue + // or flag complete + while (checkException() && state() < COMPLETE) { + if (!this.buffer.isEmpty()) + return true; + + _wait(); + } + + return !this.buffer.isEmpty(); + } + + public synchronized String[] next() throws RetsException { + checkException(); + String[] row = this.buffer.removeFirst(); + if (this.state < COMPLETE) + pushState(BUFFER_AVAILABLE); + this.notifyAll(); + return row; + } + + public synchronized int getCount() throws RetsException { + while (checkException() && state() < BUFFER_AVAILABLE) { + _wait(); + } + return this.count; + } + + public synchronized String[] getColumns() throws RetsException { + while (checkException() && state() < BUFFER_AVAILABLE) { + _wait(); + } + return this.columns; + } + + public synchronized boolean isMaxrows() throws RetsException { + checkException(); + + if (!isComplete()) + throw new IllegalStateException("Cannot call isMaxRows until isComplete == true"); + + return this.mMaxrows; + } + + public synchronized SearchResultInfo getInfo() throws RetsException { + checkException(); + + if (!isComplete()) + throw new IllegalStateException("Cannot call isMaxRows until isComplete == true"); + + return this; + } + + public synchronized boolean isComplete() throws RetsException { + checkException(); + return state() >= COMPLETE; + } + + private synchronized boolean checkRuntime() { + try { + return checkException(); + } catch (RetsException e) { + throw new RuntimeException(e); + } + } + + private synchronized boolean checkException() throws RetsException { + // considering doing something here to maintain the original + // stack trace but also provide the stack trace from this + // location... + if (this.exception != null) + throw this.exception; + return true; + } + + private void _wait() { + try { + wait(this.timeout); + } catch (InterruptedException e) { + pushState(COMPLETE); + throw new RuntimeException(e); + } + } + + private void pushState(int newState) { + if (this.state >= COMPLETE && newState < COMPLETE) + throw new IllegalStateException("Cannot revert from complete state"); + + if (this.state > PREPROCESS && newState <= PREPROCESS) + throw new IllegalStateException("Cannot revert to preprocess state"); + + if (newState < this.state && newState != BUFFER_AVAILABLE && this.state != BUFFER_FULL) + throw new IllegalStateException("Cannot go back in state unless reverting to buffer available from full"); + + this.state = newState; + } + + private int state() { + return this.state; + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/VersionInsensitiveRequest.java b/rets-io-client/src/main/java/com/ossez/usreio/client/VersionInsensitiveRequest.java new file mode 100644 index 0000000..3b72ee6 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/VersionInsensitiveRequest.java @@ -0,0 +1,15 @@ +package com.ossez.usreio.client; + +public abstract class VersionInsensitiveRequest extends RetsHttpRequest { + /** + * Abstract class of subclasses where the Version of RETS is not needed (Password Request, Login Request, etc.) + */ + public VersionInsensitiveRequest() { + super(); + } + + @Override + public void setVersion(RetsVersion version) { + //noop - I don't care about version + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/InputStreamDataSource.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/InputStreamDataSource.java new file mode 100644 index 0000000..0865d3c --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/InputStreamDataSource.java @@ -0,0 +1,126 @@ +package com.ossez.usreio.client.retsapi; + +import javax.activation.DataSource; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Vector; + +/** + * A class to provide a {@link javax.activation.DataSource} interface to + * an input stream of unknown characteristics. The DataSource + * interface requires that its implementor be able to repeatedly restart + * the read from the beginning. This isn't guaranteed by InputStream, so + * we encapsulate the InputStream with an object that will buffer the + * data coming from it. (We can't use mark/reset + * because the eventual data source consumer might use those methods, + * which would override use here. + */ +public class InputStreamDataSource implements DataSource +{ + private byte fStreamBytes[]; + private String fContentType; + + public InputStreamDataSource(InputStream baseStream, String contentType) throws IOException { + fContentType = contentType; + + // Read the content of the input stream into byte array blocks. Read + // to the end of file and save all the blocks. These will be consolidated + // after all are read. This uses twice as much storage as simply designing + // a new input stream, but I don't want to write that class right now, + // especially since I'm not completely clear on the blocking semantics. + // ByteArrayInputStream already knows them, so I'll just use that. + + Vector byteArrays = new Vector(); + int totalBytesRead = 0; + byte temporaryByteArray[]; + int readCount; + int quantum = 4096; + int bytesInCurrentBlock; + + do { + bytesInCurrentBlock = 0; + temporaryByteArray = new byte[quantum]; + do { + readCount = + baseStream.read(temporaryByteArray, bytesInCurrentBlock, quantum - bytesInCurrentBlock); + if (readCount > 0) bytesInCurrentBlock += readCount; + } while (readCount >= 0 && bytesInCurrentBlock < quantum); + + if (bytesInCurrentBlock > 0) + byteArrays.add(temporaryByteArray); + + totalBytesRead += bytesInCurrentBlock; + } while (readCount >= 0); + + // Copy all the blocks into one single mondo block. + fStreamBytes = new byte[totalBytesRead]; + + int numberOfBlocks = byteArrays.size(); + byte theBlock[]; + for (int blockIndex = 0; blockIndex < numberOfBlocks - 1; ++blockIndex) { + theBlock = (byte[]) byteArrays.get(blockIndex); + System.arraycopy(theBlock, 0, fStreamBytes, blockIndex * quantum, quantum); + } + + theBlock = (byte[]) byteArrays.get(numberOfBlocks - 1); + System.arraycopy(theBlock, 0, fStreamBytes, quantum * (numberOfBlocks - 1), bytesInCurrentBlock); + + } + + /** + * Returns the Content-Type header value for the encapsulated content. + */ + public String getContentType() { + return fContentType; + } + + /** + * Returns an input stream that may be used to access the content of this + * DataSource A new input stream, set at the beginning of the + * stream, is returned each time you call this method. + * + * @return An {@link InputStream} that will furnish the + * associated data. + */ + public InputStream getInputStream() { + return new ByteArrayInputStream(fStreamBytes); + } + + /** + * Returns the name of this data source. This class does not provide named data + * sources; the string "Untitled" is returned. + * + * @return The string "Untitled". + */ + public String getName() { + return "Untitled"; + } + + /** + * Conformance to javax.activation.DataSource Throws an + * {@link IOException} since this DataSource is read-only. + */ + public OutputStream getOutputStream() throws IOException { + throw new IOException("InputStreamDataSource is read-only."); + } + + /** + * Return the content of the input stream as a full byte array. + */ + public byte[] contentAsByteArray() { + return fStreamBytes; + } + + /** + * Returns the loaded data as a string. This is primarily for diagnostic + * purposes, as there are other ways of turning an InputStream into a String. + * + * @return A String containing the input data. + */ + public String bufferedDataAsString() { + return new String(fStreamBytes); + } +} + diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSActionTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSActionTransaction.java new file mode 100644 index 0000000..4992803 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSActionTransaction.java @@ -0,0 +1,35 @@ +package com.ossez.usreio.client.retsapi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * RETSActionTransaction.java + * + * This class is used to build an action transaction + * + * @author jbrush + * @version 1.0 + */ +public class RETSActionTransaction extends RETSTransaction { + private final static Logger logger = LoggerFactory.getLogger(RETSActionTransaction.class); + + /** + * Constructor + */ + public RETSActionTransaction() { + super(); + setRequestType("Action"); + } + + /** + * Sets the reponse body. + * + *@param body text of the response + * + */ + public void setResponse(String body) { + super.setResponse(body); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSBasicResponseParser.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSBasicResponseParser.java new file mode 100644 index 0000000..4ff1ca5 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSBasicResponseParser.java @@ -0,0 +1,128 @@ +// +// RETSBasicResponseParser.java +// NARRETSClasses +// +// Created by Bruce Toback on 1/3/05. +// Copyright (c) 2005 __MyCompanyName__. All rights reserved. +// + +package com.ossez.usreio.client.retsapi; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.InputStream; +import java.util.Vector; + +/** + * Do a basic parse of a RETS response, optionally failing fast upon determination + * as to whether this is actually a RETS response. + */ + +public class RETSBasicResponseParser extends org.xml.sax.helpers.DefaultHandler +{ + protected String fReplyText; + protected int fReplyCode; + protected Vector fExceptions; + protected StringBuffer fElementCharacters; + protected boolean fReplyValid; + protected boolean fFirstElementProcessed; // Set once the first element is processed + + public RETSBasicResponseParser(InputStream responseStream) { + fReplyValid = false; + + try { + SAXParserFactory aParserFactory = SAXParserFactory.newInstance(); + + aParserFactory.setValidating(false); + aParserFactory.setNamespaceAware(true); + + SAXParser aParser = aParserFactory.newSAXParser(); + + aParser.parse(responseStream, this); + } catch (SAXParseException saxParseException) { + addExceptionMessage(saxParseException.getMessage()); + } catch (SAXException saxException) { + addExceptionMessage(saxException.getMessage()); + } catch (ParserConfigurationException parserConfigurationException) { + addExceptionMessage(parserConfigurationException.getMessage()); + } catch (java.io.IOException ioException) { + addExceptionMessage(ioException.getMessage()); + } + } + + public boolean responseIsValid() { + return fReplyValid && (fExceptions == null || fExceptions.size() == 0); + } + + public Vector exceptionMessages() { + return fExceptions; + } + + public int replyCode() { + return fReplyCode; + } + + public String replyText() { + return fReplyText; + } + + protected void addExceptionMessage(String exceptionMessage) { + if (fExceptions == null) + fExceptions = new Vector(); + fExceptions.addElement(exceptionMessage); + } + + // Methods required to extend DefaultHandler + + public void error(SAXParseException e) { + addExceptionMessage(e.getMessage()); + } + + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + int attributeCount = attributes.getLength(); + int attributeIndex; + String attributeName; + + if (fElementCharacters == null) + fElementCharacters = new StringBuffer(); + else + fElementCharacters.setLength(0); + + if (qName.equals("RETS") || + qName.equals("RETS-STATUS")) { + for (attributeIndex = 0; attributeIndex < attributeCount; ++attributeIndex) { + attributeName = attributes.getLocalName(attributeIndex); + + if (attributeName.equals("ReplyText")) + fReplyText = attributes.getValue(attributeIndex); + else if (attributeName.equals("ReplyCode")) { + String replyCode = attributes.getValue(attributeIndex); + try { + fReplyCode = Integer.parseInt(replyCode); + fReplyValid = true; + } catch (NumberFormatException e) { + fReplyCode = 0; + addExceptionMessage("RETS reply code invalid (\"" + replyCode + "\")"); + } + } + } + } else if (!fFirstElementProcessed) { + throw new SAXException("Not a RETS reply."); + } + + fFirstElementProcessed = true; + } + + public void endElement(String uri, String localName, String qName) { + } + + public void characters(char[] ch, int start, int length) { + fElementCharacters.append(ch, start, length); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSChangePasswordTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSChangePasswordTransaction.java new file mode 100644 index 0000000..7806f23 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSChangePasswordTransaction.java @@ -0,0 +1,176 @@ +package com.ossez.usreio.client.retsapi; + + +import com.ossez.usreio.common.util.DesCrypter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.MessageDigest; + + +/** + * Send a Change Password transaction to the server. + * + * @author jbrush + * @version 1.0 + */ +public class RETSChangePasswordTransaction extends RETSTransaction { + private final static Logger logger = LoggerFactory.getLogger(RETSActionTransaction.class); + + private String oldPassword = null; + private String newPassword = null; + private String newPassword2 = null; + private String encrypted = null; + private String decrypted = null; + private String username = null; + + /** + * Create a new RETSChangePasswordTransaction + */ + public RETSChangePasswordTransaction() { + super(); + setRequestType("ChangePassword"); + } + + /** + * Sets the username + * + * @param username name user signs in with + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Sets the old password + * + * @param passwd users password to be changed + */ + public void setOldPassword(String passwd) { + this.oldPassword = passwd; + } + + /** + * Sets the new password value + * + * @param passwd new password + */ + public void setNewPassword(String passwd) { + this.newPassword = passwd; + } + + /** + * Sets the new password confirm value + * + * @param passwd new password + */ + public void setNewPassword2(String passwd) { + this.newPassword2 = passwd; + } + + /** + * process the transaction + */ + public void preprocess() { + String errMsg = null; + + super.preprocess(); + + setUsername((String) transactionContext.get("username")); + + logger.debug("username=" + username); + logger.debug("oldPassword=" + oldPassword); + logger.debug("newPassword=" + newPassword); + + //cat.debug("newPassword2="+newPassword2); + + /*if (oldPassword == null + || !oldPassword.equals(transactionContext.get("password"))) { + errMsg = "Old Password does not match."; + } + else if ((newPassword1 == null || newPassword2 == null) + || (!newPassword1.equals(newPassword2))) { + errMsg = "New Passwords do not match."; + }*/ + + //else { + String pwd = encryptPWD(); + + //cat.debug("PWD:"+pwd); + setRequestVariable("PWD", pwd); + + //} + if (errMsg != null) { + logger.warn(errMsg); + + setResponseStatus("20513"); // Miscellaneous error + setResponseStatusText(errMsg); + setResponse(errMsg); + + errMsg = null; + } + } + + public void postprocess() { + transactionContext.put("password", newPassword); + } + + private String encryptPWD() { + byte[] key = makeKey(); + String source = newPassword + ":" + username; + + return DES(key, source); + } + + private String DES(byte[] keyBytes, String source) { + try { + // Create encrypter/decrypter class + DesCrypter crypter = new DesCrypter(keyBytes); + + // Encrypt + encrypted = crypter.encrypt(source); + + // Decrypt + decrypted = crypter.decrypt(encrypted); + + return encrypted; + } catch (Exception e) { + } + + return null; + } + + private byte[] makeKey() { + MessageDigest md = null; + + try { + md = MessageDigest.getInstance("MD5"); + } catch (java.security.NoSuchAlgorithmException e) { + e.printStackTrace(); + } + + md.reset(); + + // trim to 8 bytes + byte[] key = new byte[8]; + System.arraycopy(md.digest((oldPassword + username).toUpperCase() + .getBytes()), 0, key, 0, 8); + + return key; + } + + ///////////////////////////////////////////////// + public static void main(String[] args) { + RETSChangePasswordTransaction t = new RETSChangePasswordTransaction(); + + t.setUsername(args[0]); + t.setOldPassword(args[1]); + t.setNewPassword(args[2]); + + //t.setNewPassword2(args[2]); + t.preprocess(); + + System.out.println("encrypted=" + t.encrypted); + System.out.println("decrypted=" + t.decrypted); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSConnection.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSConnection.java new file mode 100644 index 0000000..793e3ea --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSConnection.java @@ -0,0 +1,635 @@ +/* + * RETSConnection.java + * + * Created on November 16, 2001, 1:33 PM + */ +package com.ossez.usreio.client.retsapi; + +//import com.aftexsw.util.bzip.CBZip2InputStream; + +import com.ossez.usreio.common.util.RETSConfigurator; +import org.apache.commons.httpclient.*; +import org.apache.commons.httpclient.cookie.CookiePolicy; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.PostMethod; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.net.URLEncoder; +import java.util.*; +import java.util.zip.GZIPInputStream; + + +/** + * Provides a connection to a RETSServer. + * + * @author tweber + * @version 1.0 + */ +public class RETSConnection extends java.lang.Object { + private final static Logger logger = LoggerFactory.getLogger(RETSConnection.class); + + static { + RETSConfigurator.configure(); + } + + //Key value pairs for request header. + private HashMap headerHash = new HashMap(); + private HashMap responseHeaderMap = new HashMap(); + private String serverUrl = null; + private String errMsg = null; + private boolean isRetryingAuthorization = false; + private boolean gzipCompressed = false; + private boolean bzipCompressed = false; + private boolean STREAMRESPONSE = false; + private long lastTransactionTime = 0; + private String transactionLogDirectory = null; + private String imageAccept = "image/gif"; // default + private PrintWriter log = null; + private HttpClient client = new HttpClient(); + + HashMap transactionContext = new HashMap(); // holds data across transactions + private int connTimeoutSeconds = 60; // 60 seconds default + + /** + * Creates new RETSConnection and changes default connection timeout + * and sets the ServerURL. + */ + public RETSConnection(String url, int connTimeoutSeconds) { + this(url); + this.connTimeoutSeconds = connTimeoutSeconds; + } + + /** + * Creates new RETSConnection and changes default connection timeout + * and sets the ServerURL. + */ + public RETSConnection(int connTimeoutSeconds) { + this(); + this.connTimeoutSeconds = connTimeoutSeconds; + } + + /** + * Creates new RETSConnection and sets the ServerURL. + */ + public RETSConnection(String url) { + this(); + serverUrl = url; + } + + /** + * Create a new RETSConnection and setup some required Header fields. + */ + public RETSConnection() { + setRequestHeaderField("User-Agent", "Mozilla/4.0"); + setRequestHeaderField("RETS-Version", "RETS/1.0"); + } + + /** + * Executes a transaction + * + * @param transaction transaction to execute + */ + public void execute(RETSTransaction transaction) { + execute(transaction, false); + } + + /** + * Executes a transaction + * + * @param transaction transaction to execute + */ + public void executeStreamResponse(RETSTransaction transaction) { + execute(transaction, true); + } + + /** + * Executes a transaction + * + * @param transaction transaction to execute + * @param asStream + */ + public void execute(RETSTransaction transaction, boolean asStream) { + java.util.Date dt1 = new Date(); + STREAMRESPONSE = asStream; + + if (transaction instanceof RETSGetObjectTransaction) { + setRequestHeaderField("Accept", getImageAccept()); + } else { + setRequestHeaderField("Accept", "*/*"); + } + + if ((transactionLogDirectory != null) && (transactionLogDirectory.length() > 1)) { + String transType = transaction.getClass().getName(); + int nameIdx = transType.lastIndexOf(".") + 1; + String name = transType.substring(nameIdx); + Date dt = new Date(); + String outFile = transactionLogDirectory + "/" + name + dt.getTime() + ".txt"; + + try { + log = new PrintWriter(new FileWriter(outFile)); + log.println(""); + } catch (Exception e) { + logger.error("could create output file :" + outFile); + } + } + + String compressFmt = transaction.getCompressionFormat(); + + if (compressFmt != null) { + if (compressFmt.equalsIgnoreCase("gzip")) { + setRequestHeaderField("Accept-Encoding", "application/gzip,gzip"); + } else if (compressFmt.equalsIgnoreCase("bzip")) { + setRequestHeaderField("Accept-Encoding", "application/bzip,bzip"); + } else if (compressFmt.equalsIgnoreCase("none")) { + removeRequestHeaderField("Accept-Encoding"); + } + } + + transaction.setContext(transactionContext); + + transaction.preprocess(); + + processRETSTransaction(transaction); + + transaction.postprocess(); + + Date dt2 = new Date(); + lastTransactionTime = dt2.getTime() - dt1.getTime(); + + if (log != null) { + try { + log.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + log = null; + } + + return; + } + + public long getLastTransactionTime() { + return lastTransactionTime; + } + + public void setTransactionLogDirectory(String tLogDir) { + this.transactionLogDirectory = tLogDir; + } + + public String getTransactionLogDirectory() { + return this.transactionLogDirectory; + } + + private void writeToTransactionLog(String msg) { + if (log != null) { + try { + this.log.println(msg); + } catch (Exception e) { + e.printStackTrace(); + } + } + + logger.debug(msg); + } + + private void writeMapToTransactionLog(Map map) { + if (map == null) { + return; + } + + Iterator itr = map.keySet().iterator(); + + while (itr.hasNext()) { + String key = (String) itr.next(); + String value = ""; + Object obj = map.get(key); + + if (obj instanceof String) { + value = (String) obj; + } else { + value = "{ "; + + Collection c = (Collection) obj; + Iterator i2 = c.iterator(); + + if (i2.hasNext()) { + value = (String) i2.next(); + + while (i2.hasNext()) { + value = value + ", " + (String) i2.next(); + } + } + + value = value + " }"; + } + + writeToTransactionLog(key + "=" + value); + } + } + + /** + * Returns the server's URL, this url as a base for all transactions + */ + public String getServerUrl() { + return serverUrl; + } + + /** + * Sets the url for the connection. + * + * @param url Server's address ex: http://www.realtor.org/RETSServer + */ + public void setServerUrl(String url) { + serverUrl = url; + } + + /** + * Key value pairs in the client request header + * + * @param key field name in the request header + * @param value value associated with the key + */ + public void setRequestHeaderField(String key, String value) { + headerHash.put(key, value); + } + + public void setUserAgent(String userAgent) { + headerHash.put("User-Agent", userAgent); + } + + public void setRetsVersion(String retsVersion) { + setRequestHeaderField("RETS-Version", retsVersion); + } + + /** + * Removes a key/value pair from the request header. + * + * @param key field to remove from the request header. + */ + public void removeRequestHeaderField(String key) { + headerHash.remove(key); + } + + public HashMap getResponseHeaderMap() { + return responseHeaderMap; + } + + /** + * gets the url content and returns an inputstream + * + * @param strURL + * @param requestMethod + * @param requestMap + */ + public InputStream getURLContent(String strURL, String requestMethod, Map requestMap) { + InputStream is = null; + gzipCompressed = false; + bzipCompressed = false; + + boolean needToAuth = false; + + HttpMethod method = null; + + logger.debug("getURLContent: URL=" + strURL); + + try { + if (requestMethod.equalsIgnoreCase("GET")) { + method = new GetMethod(strURL); + } else { + method = new PostMethod(strURL); + } + + client.getState().setCredentials(null, null, new UsernamePasswordCredentials(getUsername(), getPassword())); + client.getState().setCookiePolicy(CookiePolicy.COMPATIBILITY); + client.setConnectionTimeout(connTimeoutSeconds * 1000); + + method.setDoAuthentication(true); +// method.setFollowRedirects(true); + + addHeaders(method, headerHash); + writeMapToTransactionLog(headerHash); + + // send the request parameters + if (requestMap != null) { + NameValuePair[] pairs = mapToNameValuePairs(requestMap); + + if (requestMethod.equalsIgnoreCase("POST")) { + // requestMethod is a post, so we can safely cast. + PostMethod post = (PostMethod) method; + post.setRequestBody(pairs); + } else { + GetMethod get = (GetMethod) method; + get.setQueryString(pairs); + } + } + + this.writeToTransactionLog(""); + + int responseCode = client.executeMethod(method); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + ByteArrayInputStream bais = new ByteArrayInputStream(method.getResponseBody()); + copyResponseHeaders(method); + method.releaseConnection(); // from bruce + return bais; + } catch (IOException io) { + io.printStackTrace(); + errMsg = "RETSAPI: I/O exception while processing transaction: " + io.getMessage(); + return null; + } finally { + if (method != null) { + method.releaseConnection(); + } + } + } + + /** + * Changes a map into an array of name value pairs + * + * @param requestMap The map to change. + * @return An array of Name value pairs, representing keys and values from the map. + */ + private NameValuePair[] mapToNameValuePairs(Map requestMap) { + NameValuePair[] pairs = new NameValuePair[requestMap.size()]; + Iterator iter = requestMap.keySet().iterator(); + int i = 0; + + while (iter.hasNext()) { + String key = (String) iter.next(); + String value = (String) requestMap.get(key); + NameValuePair nvp = new NameValuePair(key, value); + pairs[i] = nvp; + i++; + } + + return pairs; + } + + /** + * Adds response headers to Http method + * + * @param responseHeaderMap + * @param method + */ + private void copyResponseHeaders(HttpMethod method) { + responseHeaderMap.clear(); + + Header[] headers = method.getResponseHeaders(); + + for (int i = 0; i < headers.length; i++) { + Header current = headers[i]; + List list = (List) responseHeaderMap.get(current.getName()); + + if (list == null) { + list = new ArrayList(); + } + + list.add(current.getValue()); + responseHeaderMap.put(current.getName(), list); + } + } + + private void addHeaders(HttpMethod method, HashMap headers) { + Iterator keys = headers.keySet().iterator(); + + while (keys.hasNext()) { + String key = (String) keys.next(); + Object value = headers.get(key); + + if (value instanceof String && isValidString((String) value)) { + method.addRequestHeader(key, (String) value); + } else if (value instanceof ArrayList) { + ArrayList list = (ArrayList) value; + StringBuffer valueList = new StringBuffer(); + + for (int i = 0; i < list.size(); i++) { + if (i > 0) { + valueList.append(";"); + } + + valueList.append(list.get(i)); + } + + method.addRequestHeader(key, valueList.toString()); + } + } + } + + /** + * Processes a transaction, sends rets request and gets + * the response stream from the server. Uncompresses the + * response stream if compression was used in the reply + * + * @param transaction rets transaction to process + */ + private void processRETSTransaction(RETSTransaction transaction) { + try { + serverUrl = transaction.getUrl(); + + logger.debug(transaction.getRequestType() + " URL : {" + serverUrl + "}"); + + if (serverUrl == null) { + logger.error(transaction.getRequestType() + " URL is null"); + transaction.setResponseStatus("20036"); + transaction.setResponseStatusText(transaction.getRequestType() + " URL is missing. Successful login is required."); + return; // throw exception here + } + + String method = "POST"; + + // Action transaction requires a GET according to RETS spec + if (transaction.getRequestType().equalsIgnoreCase("Action")) { + method = "GET"; + } + logger.debug("method: " + method); + + InputStream is = getURLContent(serverUrl, method, transaction.getRequestMap()); + + if (is == null) { + transaction.setResponseStatus("20513"); // Miscellaneous error + transaction.setResponseStatusText(errMsg); + transaction.setResponse(errMsg); + errMsg = null; + + return; + } else { + Iterator itr = responseHeaderMap.keySet().iterator(); + Object compressionFmt = responseHeaderMap.get("Content-Encoding"); + + if (compressionFmt != null) { + logger.debug("Header class : " + compressionFmt.getClass().getName()); + + if (compressionFmt.toString().equalsIgnoreCase("[gzip]")) { + gzipCompressed = true; + } else if (compressionFmt.toString().equalsIgnoreCase("[bzip]")) { + bzipCompressed = true; + } + } + + if (gzipCompressed) { + is = new GZIPInputStream(is); + } else if (bzipCompressed) { +// is = new CBZip2InputStream(is); + } + } + this.writeToTransactionLog(""); + + transaction.setResponseHeaderMap(this.responseHeaderMap); + + if ((transaction instanceof RETSGetObjectTransaction && (!transaction.getResponseHeader("Content-Type").startsWith("text/xml"))) || STREAMRESPONSE) { + transaction.setResponseStream(is); + } else { + String contents = null; + contents = streamToString(is); + writeToTransactionLog(contents); + + /*catch( IOException e) { + errMsg = "Error reading response stream: " + contents; + cat.error(errMsg, e); + transaction.setResponseStatus("20513"); // Miscellaneous error + transaction.setResponseStatusText(errMsg); + errMsg = null; + }*/ + if (contents.length() == 0) { + transaction.setResponseStatus("20513"); // Miscellaneous error + transaction.setResponseStatusText("Empty Body"); + } + + transaction.setResponse(contents); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + String getUsername() { + String username = null; //(String)requestMap.get("username"); + + if (username == null) { + username = (String) transactionContext.get("username"); + } + + return username; + } + + String getPassword() { + String password = null; //(String)requestMap.get("password"); + + if (password == null) { + password = (String) transactionContext.get("password"); + } + + return password; + } + + /** + * Removes the quotes on a string. + * + * @param quotedString string that might contain quotes + */ + private static String removeQuotes(String quotedString) { + if ((quotedString != null) && (quotedString.length() > 2)) { + return quotedString.substring(1, quotedString.length() - 1); + } else { + return ""; // empty string + } + } + + /** + * Checks to make sure the string passed in is a valid string parameter (not null and not zero length). + * + * @param value string to be validated + */ + private boolean isValidString(String value) { + return ((value != null) && (value.length() > 0)); + } + + private String streamToString(InputStream is) throws IOException { + if (is != null) { + StringBuffer sb = new StringBuffer(); + int numread = 0; + byte[] buffer = new byte[1024 * 8]; //initialize an 8k buffer + + while ((numread = is.read(buffer)) >= 0) { + String s = new String(buffer, 0, numread); + sb.append(s); + } + + return sb.toString(); + } + + return null; + } + + /** + * Main method for testing only! + * + * @param args the command line arguments + */ + public static void main(String[] args) { +// BasicConfigurator.configure(); + + RETSConnection rc = new RETSConnection(); + RETSLoginTransaction trans = new RETSLoginTransaction(); + + try { + Properties props = new Properties(); + props.load(new FileInputStream("/tmp/client.properties")); + + // Add the optional request parameters if they exist, are non-null and non-zero-length + // rc.setRequestHeaderField("Authorization", (String)props.get("login.AUTHORIZATION")); + rc.setServerUrl((String) props.getProperty("SERVER_URL")); + trans.setUrl((String) props.getProperty("SERVER_URL")); + trans.setUsername((String) props.getProperty("USERNAME")); + trans.setPassword((String) props.getProperty("PASSWORD")); + } catch (Exception e) { + e.printStackTrace(); + } + + rc.execute(trans); + } + + /** + * Build the queryString from the request map + * + * @param requestMap the list of request parameters + */ + private String buildQueryString(Map requestMap) { + /*if (((String)(requestMap.get("requestType"))).equalsIgnoreCase("Search")) { + return "SearchType=Property&Class=RESI&Query=(Listing_Price%3D100000%2B)&QueryType=DMQL"; + }*/ + StringBuffer sb = new StringBuffer(); + Iterator it = requestMap.keySet().iterator(); + + // build query string + while (it.hasNext()) { + String key = (String) it.next(); + + if (key.equals("requestType")) { + //commenting out requestType because it is not a standard req parameter and may break RETS servers + continue; + } + + String reqStr = key + "=" + URLEncoder.encode((String) requestMap.get(key)); + logger.debug(reqStr); + sb.append(reqStr); + + if (it.hasNext()) { + sb.append("&"); + } + } + + return sb.toString(); + } + + public String getImageAccept() { + return imageAccept; + } + + public void setImageAccept(String imageAccept) { + this.imageAccept = imageAccept; + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSGetMetadataTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSGetMetadataTransaction.java new file mode 100644 index 0000000..57e91cd --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSGetMetadataTransaction.java @@ -0,0 +1,51 @@ +/** + * RETSGetMetadataTransaction.java + * + * @author jbrush + * @version + */ +package com.ossez.usreio.client.retsapi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/////////////////////////////////////////////////////////////////////// +public class RETSGetMetadataTransaction extends RETSTransaction { + private final static Logger logger = LoggerFactory.getLogger(RETSConnection.class); + String version = null; + + /** + * constructor + */ + public RETSGetMetadataTransaction() { + super(); + setRequestType("GetMetadata"); + } + + public void setType(String str) { + setRequestVariable("Type", str); + } + + public void setId(String str) { + setRequestVariable("ID", str); + } + + public void setFormat(String str) { + setRequestVariable("Format", str); + } + + // public void setResource(String str) { + // setRequestVariable("resource", str); + // } + // public void setResourceClass(String str) { + // setRequestVariable("resourceClass", str); + // } + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return version; + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSGetObjectTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSGetObjectTransaction.java new file mode 100644 index 0000000..7c9d721 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSGetObjectTransaction.java @@ -0,0 +1,339 @@ +package com.ossez.usreio.client.retsapi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.mail.MessagingException; +import javax.mail.internet.InternetHeaders; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMultipart; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +/** + * RETSGetObjectTransaction.java + * + * @author jbrush + * @version 1.0 + */ +public class RETSGetObjectTransaction extends RETSTransaction { + /** + * Encapsulates the RETS GetObject transaction, and provides services for + * interpreting the result. As with all {@link RETSTransaction} classes, + * your code should create an instance of this class when it wants to + * perform a GetObject transaction. The transaction requires three + * parameters: the resource name (see {@link #setResource}), the type + * of object requested (e.g., "Photo"; see {@link setType}), + * and the ID of the object -- that is, its associated key such as the + * MLS number or agent number. You may also request that the server + * send back only the location of the resource by calling {@link #setLocation}. + */ + + private final static Logger logger = LoggerFactory.getLogger(RETSLoginTransaction.class); + // collection of body parts resulting from the collision of the server response with the MIME parsing code. +// protected Collection fBodyParts; + protected ArrayList parts; + + /** + * create new RETSGetObjectTransaction and set RequestType + */ + public RETSGetObjectTransaction() { + super(); + setRequestType("GetObject"); + } + + /** + * Sets the response body for the transaction. + * + * @param body body of the transaction + */ + public void setResponse(String body) { + super.setResponse(body); + setKeyValuePairs(body); + } + + /** + * Sets the resource attribute to the string passed in. + * + * @param str resource value + */ + public void setResource(String str) { + logger.debug("set Resource=" + str); + setRequestVariable("Resource", str); + } + + + /** + * Sets the type attribute to the string passed in. + * + * @param str type attribute value + */ + public void setType(String str) { + logger.debug("set Type=" + str); + setRequestVariable("Type", str); + } + + /** + * Sets the ID attribute to the string passed in. + * + * @param str ID of the object + */ + public void setID(String str) { + if (str != null) { + logger.debug("set ID=" + str.trim()); + setRequestVariable("ID", str.trim()); + } else { + logger.debug("set ID=" + str); + } + } + + /** + * Sets the location attribute to the string passed in. + * + * @param str location attribute value + */ + public void setLocation(String str) { + logger.debug("set Location=" + str); + setRequestVariable("Location", str); + } + + /** + * Sets the response stream. This triggers various actions depending on the + * content-type of the response stream: + *

+ * If the content-type is text/plain or text/xml, then assume we have a + * RETS response and parse it accordingly. Note that we'll not have + * anything in the RETS response except error information. (We might + * have no error and nothing else, in which case the only possibility + * is that we made a request with Location=1 and the server has + * responded in kind.) We still make this available, in case there *is* + * something else in the response. + *

+ * A content-type of multipart, with any subtype, is interpreted as a + * multipart response. This is parsed to create a list of MIME parts. + * Any other content type is simply made available as a single MIME part. + *

+ * This method is called by {@link RETSConnection} to provide access to + * the response from a transaction. You don't normally call this method + * yourself. + * + * @param responseStream The response stream to associate with this transaction. + * Rarely. You can override this method if you want to provide + * special processing of a GetObject response stream, but + * it will usually be more convenient to override one + * of the methods that handles particular MIME types. + */ + public void setResponseStream(InputStream responseStream) { + + String mimeType; +// String contentType = responseHeaderNamed("content-type"); + String contentType = super.getResponseHeader("Content-Type"); + logger.debug("====[RETSGetObjectTx] --> " + contentType); + int contentTypeSemicolonIndex = + contentType == null ? -1 : contentType.indexOf(";"); + + // If there was no Content-Type header, we can't do anything here. Punt to the default handler. + if (contentType == null) { + logger.debug("====[RETSGetObjectTx] : NO CONTENT TYPE"); + super.setResponseStream(responseStream); + return; + } + + // If the content-type string had parameters, trim them to get just the MIME type. + if (contentTypeSemicolonIndex >= 0) + mimeType = contentType.substring(0, contentTypeSemicolonIndex).trim(); + else + mimeType = contentType.trim(); + + logger.debug("====[RETSGetObjectTx] : mime-type -> " + mimeType); + + // If the type is text/xml, then this is probably an error response. + // We need to parse the input stream nondestructively to find out. + if (mimeType.equals("text/xml")) { + handleXMLStream(responseStream, mimeType, contentType); + } else if (mimeType.startsWith("multipart/")) { + // If it's multipart, take it apart and set up appropriate data structures. + logger.debug("====[RETSGetObjectTx] : RECIEVED MULTIPART"); + handleMultipartStream(responseStream, mimeType, contentType); + } else { + // Otherwise, since we do have a MIME type, assume that the response *is* object value. + handleStreamWithType(responseStream, mimeType, contentType); + } + } + + /** + * Handle an input stream whose advertised MIME type is text/xml. + * This may be a RETS error response or something else; we need to figure + * out exactly what it is. If it is a RETS response, parse it and + * deal with it. If not, handle as for an unknown response. + * + * @param responseStream The response stream containing the XML data. + * Override to provide your own handling of XML data + * streams. + */ + protected void handleXMLStream(InputStream responseStream, String mimeType, String contentType) { + try { + InputStreamDataSource responseStreamDataSource = + new InputStreamDataSource(responseStream, contentType); + RETSBasicResponseParser basicResponseParser = + new RETSBasicResponseParser(responseStreamDataSource.getInputStream()); + if (basicResponseParser.responseIsValid()) { + setResponseStatus(Integer.toString(basicResponseParser.replyCode())); + setResponseStatusText(basicResponseParser.replyText()); + } else { + makeSinglePartFromByteStream(responseStreamDataSource.contentAsByteArray(), contentType); + setResponseStatus("0"); // The response is valid in this case, since we got a body that we can provide + } + } catch (Exception e) { + // We really need something better for this. + setResponseStatus("20513"); + setResponseStatusText("RETSAPI: Could not create a MIME body part from XML stream: " + e.getMessage()); + } + } + + /** + * Handle an input stream whose advertised MIME type is multipart. + * This involves breaking up the stream into its constituent parts + * for callers to retrieve. + * + * @param responseStream The stream to parse. + * @param mimeType The MIME type and subtype associated with the stream. + * @param contentType The Content-Type header, including the MIME type and its parameters, if any + */ + protected void handleMultipartStream(InputStream responseStream, String mimeType, String contentType) { + + InputStreamDataSource responseStreamDataSource = null; + try { + responseStreamDataSource = new InputStreamDataSource(responseStream, contentType); + MimeMultipart multipartResponse = new MimeMultipart(responseStreamDataSource); +// multipartResponse.writeTo(System.err); + + parts = new ArrayList(); + int partCount = multipartResponse.getCount(); + for (int i = 0; i < partCount; ++i) { + parts.add(multipartResponse.getBodyPart(i)); + + } + + setResponseStatus("0"); + } catch (MessagingException messagingException) { + if (responseStreamDataSource != null) + logger.debug(responseStreamDataSource.bufferedDataAsString()); +// System.out.println(responseStreamDataSource.bufferedDataAsString()); + + messagingException.printStackTrace(); + setResponseStatus("20513"); + setResponseStatusText("RETSAPI: Could not create a multipart stream from response: " + messagingException.getMessage()); + } catch (IOException ioException) { + ioException.printStackTrace(); + setResponseStatus("20513"); + setResponseStatusText("RETSAPI: I/O exception while creating multipart stream from response: " + ioException.getMessage()); + } finally { + // We always want at least an empty body part list. + if (parts == null) parts = new ArrayList(); + } + } + + /** + * Helper for making the response into a single body part. This takes an + * byte array which may have been created during an earlier + * phase of processing, rather than taking an InputStream. + * + * @param inputBytes A byte array containing the response data. + * @param contentType The content-type header. + */ + protected void makeSinglePartFromByteStream(byte[] inputBytes, String contentType) { + // First, we need to gather the response headers into an InternetHeader object + InternetHeaders originalHeaders = new InternetHeaders(); + Iterator headerIterator = getResponseHeaderMap().keySet().iterator(); + + Object headerContent = null; + String headerName = null; + while (headerIterator.hasNext()) { + headerName = (String) headerIterator.next(); +// String headerContent = (String) getResponseHeaderMap().get(headerName); + headerContent = getResponseHeaderMap().get(headerName); + if (headerContent != null) + originalHeaders.addHeader(headerName, headerContent.toString()); + } + parts = new ArrayList(1); // We may not have *any*, but we won't have more than 1. + try { + parts.add(new MimeBodyPart(originalHeaders, inputBytes)); + setResponseStatus("0"); + } catch (Exception e) { + e.printStackTrace(); + setResponseStatus("20513"); + setResponseStatusText("RETSAPI: Could not create a MIME body part from response: " + e.getMessage()); + } + } + + /** + * Handle an input stream whose advertised MIME type isn't either + * multipart or XML. This packages up the stream as its own MIME part + * in order to offer it through the normal multipart interface. + * + * @param responseStream The stream to parse. + * @param mimeType The MIME type and subtype associated with the stream. + * @param contentType The Content-Type header, including the MIME type and its parameters, if any. + *

+ * MIME types. + */ + protected void handleStreamWithType(InputStream responseStream, String mimeType, String contentType) { + try { + makeSinglePartFromByteStream( + (new InputStreamDataSource(responseStream, contentType)).contentAsByteArray(), contentType); + } catch (IOException e) { + e.printStackTrace(); + setResponseStatus("20513"); + setResponseStatusText("RETSAPI: Could not create a data source from response: " + e.getMessage()); + } + } + + /** + * Returns the count of retrieved objects. + */ + public int getObjectCount() { + if (parts == null) return 0; + return parts.size(); + } + + /** + * Returns a vector of all objects found. These are stored as MimeBodyPart objects. + * The returned vector may be empty. + */ + public Collection allReturnedObjects() { + return parts; + } + + /** + * Returns the object with the given index as a MIME body part. Returns null + * if no object with the given index exists. + * + * @param objectIndex The index of the object to retrieve. + */ + public MimeBodyPart partAtIndex(int objectIndex) { + if (parts == null || objectIndex < 0 || objectIndex >= parts.size()) + return null; + return (MimeBodyPart) parts.get(objectIndex); + } + + public InputStream getPartAsStream(int objectIndex) { + InputStream inputStream = null; + MimeBodyPart part = this.partAtIndex(objectIndex); + try { + if (part != null) { + Object content = part.getContent(); + inputStream = (InputStream) content; + logger.debug("--- MimeBodyPart Content--> " + content); + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Kablewie!"); + } + return inputStream; + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSLoginTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSLoginTransaction.java new file mode 100644 index 0000000..75ac5af --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSLoginTransaction.java @@ -0,0 +1,184 @@ +package com.ossez.usreio.client.retsapi; + + +//import org.apache.regexp.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + + +/** + * RETSLoginTransaction.java + * + * + * @author jbrush + * @version 1.0 + */ +public class RETSLoginTransaction extends RETSTransaction { + /** log4j Object */ + private final static Logger logger = LoggerFactory.getLogger(RETSLoginTransaction.class); + + /** + * capablitiy list + */ + private static final String[] capList = { + "Login", // Login first because it might reset URL root + "Action", "ChangePassword", "GetObject", "LoginComplete", "Logout", + "Search", "GetMetadata", "Update", "ServerInformation" + }; + String url = null; + String version = null; + + public RETSLoginTransaction() { + super(); + setRequestType("Login"); + } + + /** Sets the response body. This method is called from RETSConnection.execute() + * @param body body of the response + */ + public void setResponse(String body) { + super.setResponse(body); + + setKeyValuePairs(body); + + setCapabilityUrls(); + } + + /** Sets the username for this transaction + * @param username the user's login name + */ + public void setUsername(String username) { + setRequestVariable("username", username); + } + + /** sets the User's password for this transaction. + * @param password password string + */ + public void setPassword(String password) { + setRequestVariable("password", password); + } + + /** sets the URL for this transaction. + * @param url url string + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * gets the URL for this transaction. + * @return URL String + */ + public String getUrl() { + return url; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return version; + } + + /** + * + */ + public void preprocess() { + super.preprocess(); + + // save the username and password + transactionContext.put("username", getRequestVariable("username")); + transactionContext.put("password", getRequestVariable("password")); + } + + /** Extracts the capabilitiesUrls out of the response body. + * + */ + void setCapabilityUrls() { + Map respMap = getResponseMap(); + Set respKeySet = respMap.keySet(); + Iterator iter = null; + String key = null; + + int capLength = capList.length; + + String urlRoot = getUrlRoot(url); // set root to current url root + String capUrl = null; + String qualifiedUrl = null; + + /* jump thru hoop because key might not be in proper mixed-case so we need to map it */ + for (int i = 0; i < capLength; i++) { + iter = respKeySet.iterator(); + + while (iter.hasNext()) { + key = (String) iter.next(); + + if (capList[i].equalsIgnoreCase(key)) { + capUrl = getResponseVariable(key); + qualifiedUrl = qualifyUrl(capUrl, urlRoot); + + logger.debug(capList[i] + "=[" + qualifiedUrl + "]"); + putCapabilityUrl(capList[i], qualifiedUrl); + + if (capList[i].equalsIgnoreCase("Login")) // login may reset rootUrl + { + urlRoot = getUrlRoot(qualifiedUrl); + } + + break; + } + } + } + } + + /** + * Makes sure url is fully qualified. + */ + private String qualifyUrl(String url, String defaultRoot) { + String root = getUrlRoot(url); + String sep = ""; + + if (root == null) { + if (url.charAt(0) != '/') { + sep = "/"; + } + + return defaultRoot + sep + url; + } else { + return url; + } + } + + String getUrlRoot(String myUrl) { + try { + URL url = new URL(myUrl); + + String protocol = url.getProtocol(); + String host = url.getHost(); + int port = url.getPort(); + + //String path = url.getPath(); + //String file = url.getFile(); + logger.debug("protocol = [" + protocol + "]"); + logger.debug("host = [" + host + "]"); + logger.debug("port = [" + port + "]"); + + //cat.debug("path = ["+path+"]"); + //cat.debug("file = ["+file+"]"); + return protocol + "://" + host + ((port > 0) ? (":" + port) : ""); + } catch (MalformedURLException e) { + logger.warn("getUrlRoot:MalformedURLException myUrl=\"" + myUrl + + "\""); + } + + return null; + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSLogoutTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSLogoutTransaction.java new file mode 100644 index 0000000..e560a86 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSLogoutTransaction.java @@ -0,0 +1,32 @@ +package com.ossez.usreio.client.retsapi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Send a logout transaction to the server. + * + * @author jbrush + * @version 1.0 + */ +public class RETSLogoutTransaction extends RETSTransaction { + private final static Logger logger = LoggerFactory.getLogger(RETSLogoutTransaction.class); + + /** Create a new RETSLogoutTransaction */ + public RETSLogoutTransaction() { + super(); + setRequestType("Logout"); + } + + /** Sets the response body. Called from RETSConnection.execute() + * after the logout transaction is executed. + * + * @param body Body of the response to the RETSLogoutTransaction. + */ + public void setResponse(String body) { + super.setResponse(body); + + setKeyValuePairs(body); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchAgentTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchAgentTransaction.java new file mode 100644 index 0000000..b39da36 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchAgentTransaction.java @@ -0,0 +1,32 @@ +package com.ossez.usreio.client.retsapi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * RETSSearchAgentTransaction.java + * Search for agents + * + * @author jbrush + * @version 1.0 + */ +public class RETSSearchAgentTransaction extends RETSSearchTransaction { + private final static Logger logger = LoggerFactory.getLogger(RETSSearchAgentTransaction.class); + + /**create a new RETSSearchAgentTransaction*/ + public RETSSearchAgentTransaction() { + super(); + setSearchType("Agent"); + setSearchClass("Agent"); + } + + /** Search by last name, pass in the lastname of a user + * as the "query" argument. + * @param searchByLastname lastname of the user to search. + */ + public void setSearchByLastname(String searchByLastname) { + // convert to DMQL + setSearchQuery("(LastName=" + searchByLastname + ")"); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchOfficeTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchOfficeTransaction.java new file mode 100644 index 0000000..f4a3324 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchOfficeTransaction.java @@ -0,0 +1,27 @@ +package com.ossez.usreio.client.retsapi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * RETSSearchOfficeTransaction.java + * Performs a getOffice Transaction + * + * @author jbrush + * @version 1.0 + */ +public class RETSSearchOfficeTransaction extends RETSSearchTransaction { + private final static Logger logger = LoggerFactory.getLogger(RETSSearchOfficeTransaction.class); + + /** Creates new a RETSSearchOfficeTransaction + * + */ + public RETSSearchOfficeTransaction() { + super(); + setSearchType("Office"); + + setSearchClass("Office"); +// setSearchClass("OFF"); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchPropertyBatchTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchPropertyBatchTransaction.java new file mode 100644 index 0000000..3fafce8 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchPropertyBatchTransaction.java @@ -0,0 +1,26 @@ +package com.ossez.usreio.client.retsapi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * RETSSearchPropertyBatchTransaction.java + * + * @author jbrush + * @version 1.0 + */ +public class RETSSearchPropertyBatchTransaction extends RETSSearchTransaction { + private final static Logger logger = LoggerFactory.getLogger(RETSSearchPropertyBatchTransaction.class); + + public RETSSearchPropertyBatchTransaction() { + super(); + setSearchType("Property"); + setSearchClass("RES"); + } + + public void setSearchByListingAgent(String agent) { + // convert to DMQL + setSearchQuery("(AgentID=" + agent + ")"); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchPropertyTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchPropertyTransaction.java new file mode 100644 index 0000000..25bae28 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchPropertyTransaction.java @@ -0,0 +1,20 @@ +package com.ossez.usreio.client.retsapi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * RETSSearchPropertyBatchTransaction.java + * + * @author jbrush + * @version 1.0 + */ +public class RETSSearchPropertyTransaction extends RETSSearchTransaction { + private final static Logger logger = LoggerFactory.getLogger(RETSSearchPropertyTransaction.class); + + public RETSSearchPropertyTransaction() { + super(); + setSearchType("Property"); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchTransaction.java new file mode 100644 index 0000000..cd8b816 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSSearchTransaction.java @@ -0,0 +1,321 @@ +package com.ossez.usreio.client.retsapi; + +import com.ossez.usreio.common.util.AttributeExtracter; +import com.ossez.usreio.common.util.ResourceLocator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.ByteArrayInputStream; +import java.util.HashMap; + +/** + * RETSSearchTransaction + * + * @author YuCheng Hu + */ +public class RETSSearchTransaction extends RETSTransaction { + private final static Logger logger = LoggerFactory.getLogger(RETSSearchTransaction.class); + + //Required Arguments + protected static final String SEARCHTYPE = "SearchType"; + protected static final String SEARCHCLASS = "Class"; + protected static final String SEARCHQUERY = "Query"; + protected static final String SEARCHQUERYTYPE = "QueryType"; + + // Optional Arguments + protected static final String SEARCHCOUNT = "Count"; + protected static final String SEARCHFORMAT = "Format"; + protected static final String SEARCHLIMIT = "Limit"; + protected static final String SEARCHOFFSET = "Offset"; + protected static final String SEARCHSELECT = "Select"; + protected static final String SEARCHDELIMITER = "DELIMITER"; + protected static final String SEARCHRESTRICTEDINDICATOR = "RestrictedIndicator"; + protected static final String SEARCHSTANDARDNAMES = "StandardNames"; + private String version = null; + + public RETSSearchTransaction() { + super(); + setRequestType("Search"); + setSearchQueryType("DMQL"); + } + + public void setResponse(String body) { + super.setResponse(body); + + HashMap hm = this.getAttributeHash(body); + processXML(hm); + } + + /////////////////////////////////////////////////////////////////////// + + /* void processCompact(String body) { + processCountTag(body); + processDelimiterTag(body); + processColumnTag(body); + processCompactData(body); + processMaxRowTag(body); + } */ + void processXML(HashMap hash) { + if (hash == null) { + return; + } + + processCountTag((HashMap) hash.get("COUNT")); + processXMLData((HashMap) hash.get("BODY")); + processMaxRowTag((HashMap) hash.get("MAXROWS")); + processDelimiterTag((HashMap) hash.get("DELIMITER")); + } + + private HashMap getAttributeHash(String body) { + AttributeExtracter ae = new AttributeExtracter(); + DefaultHandler h = ae; + + try { + SAXParserFactory spf = SAXParserFactory.newInstance(); + SAXParser p = spf.newSAXParser(); + ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes()); + + p.parse(bais, h, "file:/" + ResourceLocator.locate("dummy.dtd")); + } catch (Exception e) { + logger.warn("Hash Error:", e); + + return null; + } + + return ae.getHash(); + } + + void processCountTag(HashMap hash) { + if (hash == null) { + return; + } + + String records = (String) hash.get("Records"); + if (records == null) { + records = (String) hash.get("records"); + } + if (records == null) { + records = (String) hash.get("RECORDS"); + } + setCount(records); + } + + void processDelimiterTag(HashMap hash) { + if (hash == null) { + return; + } + + String delim = (String) hash.get("value"); + + if (delim == null) { + delim = (String) hash.get("VALUE"); + } + + if (delim == null) { + delim = (String) hash.get("Value"); + } + + setSearchDelimiter(delim); + } + + void processColumnTag(HashMap hash) { + } + + void processCompactData(HashMap hash) { + } + + void processXMLData(HashMap hash) { + } + + void processMaxRowTag(HashMap hash) { + if (hash == null) { + setResponseVariable("MAXROWS", "true"); + } + + // else + // setResponseVariable("MAXROWS", "false"); + } + + /////////////////////////////////////////////////////////////////////// + public void setSearchType(String searchType) { + setRequestVariable(SEARCHTYPE, searchType); + } + + public String getSearchType() { + return getRequestVariable(SEARCHTYPE); + } + + /////////////////////////////////////////////////////////////////////// + public void setSearchClass(String searchClass) { + setRequestVariable(SEARCHCLASS, searchClass); + } + + public String getSearchClass() { + return getRequestVariable(SEARCHCLASS); + } + + /////////////////////////////////////////////////////////////////////// + public void setSearchQuery(String searchQuery) { + setRequestVariable(SEARCHQUERY, searchQuery); + } + + public String getSearchQuery() { + return getRequestVariable(SEARCHQUERY); + } + + public void setQuery(String searchQuery) { + setRequestVariable(SEARCHQUERY, searchQuery); + } + + public String getQuery() { + return getRequestVariable(SEARCHQUERY); + } + + /////////////////////////////////////////////////////////////////////// + public void setSearchQueryType(String searchQueryType) { + setRequestVariable(SEARCHQUERYTYPE, searchQueryType); + } + + public String getSearchQueryType() { + return getRequestVariable(SEARCHQUERYTYPE); + } + + public void setQueryType(String searchQueryType) { + setRequestVariable(SEARCHQUERYTYPE, searchQueryType); + } + + public String getQueryType() { + return getRequestVariable(SEARCHQUERYTYPE); + } + + /////////////////////////////////////////////////////////////////////// + public void setSearchCount(String value) { + setRequestVariable(SEARCHCOUNT, value); + } + + public String getSearchCount() { + return getRequestVariable(SEARCHCOUNT); + } + + public void setCount(String value) { + setRequestVariable(SEARCHCOUNT, value); + } + + public String getCount() { + return getRequestVariable(SEARCHCOUNT); + } + + /////////////////////////////////////////////////////////////////////// + public void setSearchFormat(String value) { + setRequestVariable(SEARCHFORMAT, value); + } + + public String getSearchFormat() { + return getRequestVariable(SEARCHFORMAT); + } + + public void setFormat(String value) { + setRequestVariable(SEARCHFORMAT, value); + } + + public String getFormat() { + return getRequestVariable(SEARCHFORMAT); + } + + /////////////////////////////////////////////////////////////////////// + public void setSearchLimit(String value) { + setRequestVariable(SEARCHLIMIT, value); + } + + public String getSearchLimit() { + return getRequestVariable(SEARCHLIMIT); + } + + public void setLimit(String value) { + setRequestVariable(SEARCHLIMIT, value); + } + + public String getLimit() { + return getRequestVariable(SEARCHLIMIT); + } + + /////////////////////////////////////////////////////////////////////// + public void setSearchOffset(String value) { + setRequestVariable(SEARCHOFFSET, value); + } + + public String getSearchOffset() { + return getRequestVariable(SEARCHOFFSET); + } + + public void setOffset(String value) { + setRequestVariable(SEARCHOFFSET, value); + } + + public String getOffset() { + return getRequestVariable(SEARCHOFFSET); + } + + /////////////////////////////////////////////////////////////////////// + public void setSearchSelect(String value) { + setRequestVariable(SEARCHSELECT, value); + } + + public String getSearchSelect() { + return getRequestVariable(SEARCHSELECT); + } + + public void setSelect(String value) { + setRequestVariable(SEARCHSELECT, value); + } + + public String getSelect() { + return getRequestVariable(SEARCHSELECT); + } + + /////////////////////////////////////////////////////////////////////// + public void setSearchDelimiter(String value) { + setRequestVariable(SEARCHDELIMITER, value); + } + + public String getSearchDelimiter() { + return getRequestVariable(SEARCHDELIMITER); + } + + /////////////////////////////////////////////////////////////////////// + public void setSearchRestrictedIndicator(String value) { + setRequestVariable(SEARCHRESTRICTEDINDICATOR, value); + } + + public String getSearchRestrictedIndicator() { + return getRequestVariable(SEARCHRESTRICTEDINDICATOR); + } + + /////////////////////////////////////////////////////////////////////// + public void setSearchStandardNames(String value) { + setRequestVariable(SEARCHSTANDARDNAMES, value); + } + + public String getSearchStandardNames() { + return getRequestVariable(SEARCHSTANDARDNAMES); + } + + public void setStandardNames(String value) { + setRequestVariable(SEARCHSTANDARDNAMES, value); + } + + public String getStandardNames() { + return getRequestVariable(SEARCHSTANDARDNAMES); + } + + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return version; + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSServerInformationTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSServerInformationTransaction.java new file mode 100644 index 0000000..43e6db9 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSServerInformationTransaction.java @@ -0,0 +1,50 @@ +/** + * RETSServerInformationTransaction.java + * + * @author pobrien + * @version + */ +package com.ossez.usreio.client.retsapi; + + +//import java.util.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/////////////////////////////////////////////////////////////////////// +public class RETSServerInformationTransaction extends RETSTransaction { + private final static Logger logger = LoggerFactory.getLogger(RETSServerInformationTransaction.class); + String version = null; + + /** + * constructor + */ + public RETSServerInformationTransaction() { + super(); + setRequestType("ServerInformation"); + } + + public void setResource(String str) { + setRequestVariable("Resource", str); + } + + public void setInfoClass(String str) { + setRequestVariable("Class", str); + } + + public void setStandardNames(String str) { + setRequestVariable("StandardNames", str); + } + + + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return version; + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSTransaction.java new file mode 100644 index 0000000..8f6e844 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSTransaction.java @@ -0,0 +1,277 @@ +/** + * RETSTransaction.java + * + * @author jbrush + * @version + */ +package com.ossez.usreio.client.retsapi; + +import com.ossez.usreio.common.util.RETSRequestResponse; +import org.apache.regexp.RE; +import org.apache.regexp.RESyntaxException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.StringTokenizer; + + +/////////////////////////////////////////////////////////////////////// + +public class RETSTransaction extends RETSRequestResponse { + private final static Logger logger = LoggerFactory.getLogger(RETSConnection.class); + + private static final String STATUS = "status"; + private static final String STATUSTEXT = "statusText"; + private static final String BODY = "body"; + private static final String REQUESTTYPE = "requestType"; + protected static final String RESOURCE = "Resource"; + protected static final String CLASS_NAME = "ClassName"; + protected HashMap transactionContext = null; + private HashMap capabilityUrls = null; + protected HashMap responseHeaderMap = null; + InputStream responseStream = null; + RE firstStatusRE = null; + RE secondStatusRE = null; + + /** + * Holds value of property compressionFormat. + */ + private String compressionFormat = null; + + public RETSTransaction() { + super(); + + try { + // TODO: RE's should be precompiled + firstStatusRE = new RE("= 0) { + setResponseVariable(line.substring(0, equalSign).trim(), + line.substring(equalSign + 1).trim()); + } + } + } + } + + /////////////////////////////////////////////////////////////////////// + void putCapabilityUrl(String key, String value) { + if (capabilityUrls == null) { + capabilityUrls = new HashMap(); + } + + capabilityUrls.put(key, value); + } + + public String getCapabilityUrl(String key) { + return (String) capabilityUrls.get(key); + } + + /////////////////////////////////////////////////////////////////////// + public void preprocess() { + // by default does nothing + //subclasses can override + } + + public void postprocess() { + // by default does nothing + //subclasses can override + } + + void setContext(HashMap transactionContext) { + if (transactionContext != null) { + this.transactionContext = transactionContext; + + capabilityUrls = (HashMap) transactionContext.get("capabilityUrls"); + + if (capabilityUrls == null) { + capabilityUrls = new HashMap(); + transactionContext.put("capabilityUrls", capabilityUrls); + } + } + } + + public HashMap getTransactionContext() { + return transactionContext; + } + + public HashMap getResponseHeaderMap() { + return responseHeaderMap; + } + + public void setResponseHeaderMap(HashMap responseHeaders) { + responseHeaderMap = responseHeaders; + } + + /** + * Returns the value of the response header with the specified name, or null + * if the header was not returned. + * + * @param headerName The name of the header to be retrieved. + */ + public String getResponseHeader(String headerName) { + String responseString = null; + // If we have no header map, we obviously have no headers. Also, if + // there is no list for the header name, we don't have the + // requested header. + if (headerName != null && headerName.equals("content-type")) { + headerName = "Content-Type"; + } + if (responseHeaderMap != null) { + logger.debug("RESPONSEHEADERMAP ==> " + responseHeaderMap.toString()); +// responseString = (String) responseHeaderMap.get(headerName.toLowerCase()); + logger.debug("ContentType Class is ... " + responseHeaderMap.get(headerName).getClass().getName()); + Object object = responseHeaderMap.get(headerName); + if (object == null) + return null; + if (object instanceof ArrayList) { + responseString = (String) ((ArrayList) object).get(0); + } else + responseString = object.toString(); + } else { + logger.debug("RESPONSEHEADERMAP ==> " + responseHeaderMap); + } + return responseString; + } + + /** + * Getter for property compressionFormat. + * + * @return Value of property compressionFormat. + */ + public String getCompressionFormat() { + return this.compressionFormat; + } + + /** + * Setter for property compressionFormat. + * + * @param compressionFormat New value of property compressionFormat. + */ + public void setCompressionFormat(String compressionFormat) { + this.compressionFormat = compressionFormat; + } + + static public void log(String logMessage) { + logger.debug(logMessage); + } +} + + diff --git a/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSUpdateTransaction.java b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSUpdateTransaction.java new file mode 100644 index 0000000..edde09c --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/client/retsapi/RETSUpdateTransaction.java @@ -0,0 +1,156 @@ +package com.ossez.usreio.client.retsapi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Iterator; +import java.util.Map; + + +/** + * RETSUpdateTransaction.java + * + * @author pobrien + * @version 1.0 + */ +public class RETSUpdateTransaction extends RETSTransaction { + private final static Logger logger = LoggerFactory.getLogger(RETSUpdateTransaction.class); + + /** + * + */ + public RETSUpdateTransaction() { + super(); + setRequestType("Update"); + setDelimiter("09");//default is ascii ht + } + + /** + * Sets the response body for the transaction. + * + * @param body body of the transaction + */ + public void setResponse(String body) { + super.setResponse(body); + System.out.println("Setting response as " + body); + setKeyValuePairs(body); + } + + + /** + * Sets the type attribute to the string passed in. + * + * @param str type attribute value + */ + public void setType(String str) { + logger.debug("set Type=" + str); + setRequestVariable("Type", str); + } + + /** + * Sets the ID attribute to the string passed in. + * + * @param str ID of the object + */ + public void setValidate(String str) { + logger.debug("set Validate=" + str); + setRequestVariable("Validate", str); + } + + /** + * Sets the location attribute to the string passed in. + * + * @param str location attribute value + */ + public void setDelimiter(String str) { + logger.debug("set Delimiter=" + str); + setRequestVariable("Delimiter", str); + } + + public String getDelimiter() { + return getRequestVariable("Delimiter"); + } + + public void setRecord(String str) { + logger.debug("set Record=" + str); + setRequestVariable("Record", str); + } + + public void setWarningResponse(String str) { + logger.debug("set WarningResponse=" + str); + setRequestVariable("WarningResponse", str); + } + + public void setNewValues(Map m) { + // convert to a string and feed to setRecord().... + StringBuffer record = new StringBuffer(); + Iterator iter = m.keySet().iterator(); + // delimiter is a 2 digit HEX value + char delim = (char) Integer.parseInt(getDelimiter().trim(), 16); + + while (iter.hasNext()) { + String name = (String) iter.next(); + Object val = m.get(name); + String value = ""; + + if (val instanceof String) { + value = (String) val; + } else { + String[] arr = (String[]) val; + value = arr[0]; + } + + record.append(name); + record.append("="); + record.append(value); + + if (iter.hasNext()) { + + record.append(delim); + } + } + + setRecord(record.toString()); + } + + + public void setWarningResponseValues(Map m) { + // convert to a string and feed to setWarningResponse().... + StringBuffer warning = new StringBuffer("("); + Iterator iter = m.keySet().iterator(); + // delimiter is a 2 digit HEX value + char delim = (char) Integer.parseInt(getDelimiter().trim(), 16); + + while (iter.hasNext()) { + String name = (String) iter.next(); + Object val = m.get(name); + String value = ""; + + if (val instanceof String) { + value = (String) val; + } else { + String[] arr = (String[]) val; + value = arr[0]; + } + + warning.append(name); + warning.append("="); + warning.append(value); + + if (iter.hasNext()) { + + warning.append(delim); + } + } + + warning.append(")"); + setWarningResponse(warning.toString()); + } + + public void setUID(String id) { + System.out.println("UID is " + id); + setRequestVariable("OriginalUid", id); + } + + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/AttrType.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/AttrType.java new file mode 100644 index 0000000..dc4a680 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/AttrType.java @@ -0,0 +1,16 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata; + +import java.io.Serializable; + +public interface AttrType extends Serializable { + public T parse(String value, boolean strict) throws MetaParseException; + public Class getType(); + public String render(T value); +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/JDomCompactBuilder.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/JDomCompactBuilder.java new file mode 100644 index 0000000..d231522 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/JDomCompactBuilder.java @@ -0,0 +1,702 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata; + +import java.util.LinkedList; +import java.util.List; +import java.util.StringTokenizer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.dom4j.*; + +import com.ossez.usreio.tests.common.metadata.types.MClass; +import com.ossez.usreio.tests.common.metadata.types.MEditMask; +import com.ossez.usreio.tests.common.metadata.types.MLookup; +import com.ossez.usreio.tests.common.metadata.types.MLookupType; +import com.ossez.usreio.tests.common.metadata.types.MObject; +import com.ossez.usreio.tests.common.metadata.types.MResource; +import com.ossez.usreio.tests.common.metadata.types.MSearchHelp; +import com.ossez.usreio.tests.common.metadata.types.MSystem; +import com.ossez.usreio.tests.common.metadata.types.MTable; +import com.ossez.usreio.tests.common.metadata.types.MUpdate; +import com.ossez.usreio.tests.common.metadata.types.MUpdateType; +import com.ossez.usreio.tests.common.metadata.types.MValidationExpression; +import com.ossez.usreio.tests.common.metadata.types.MValidationExternal; +import com.ossez.usreio.tests.common.metadata.types.MValidationExternalType; +import com.ossez.usreio.tests.common.metadata.types.MValidationLookup; +import com.ossez.usreio.tests.common.metadata.types.MValidationLookupType; +import org.dom4j.io.SAXReader; +import org.xml.sax.InputSource; + +public class JDomCompactBuilder extends MetadataBuilder { + public static final String CONTAINER_PREFIX = "METADATA-"; + public static final String CONTAINER_ROOT = "RETS"; + public static final String CONTAINER_METADATA = "METADATA"; + public static final String CONTAINER_SYSTEM = "METADATA-SYSTEM"; + public static final String CONTAINER_RESOURCE = "METADATA-RESOURCE"; + public static final String CONTAINER_FOREIGNKEY = "METADATA-FOREIGN_KEY"; + public static final String CONTAINER_CLASS = "METADATA-CLASS"; + public static final String CONTAINER_TABLE = "METADATA-TABLE"; + public static final String CONTAINER_UPDATE = "METADATA-UPDATE"; + public static final String CONTAINER_UPDATETYPE = "METADATA-UPDATE_TYPE"; + public static final String CONTAINER_OBJECT = "METADATA-OBJECT"; + public static final String CONTAINER_SEARCHHELP = "METADATA-SEARCH_HELP"; + public static final String CONTAINER_EDITMASK = "METADATA-EDITMASK"; + public static final String CONTAINER_UPDATEHELP = "METADATA-UPDATE_HELP"; + public static final String CONTAINER_LOOKUP = "METADATA-LOOKUP"; + public static final String CONTAINER_LOOKUPTYPE = "METADATA-LOOKUP_TYPE"; + public static final String CONTAINER_VALIDATIONLOOKUP = "METADATA-VALIDATION_LOOKUP"; + public static final String CONTAINER_VALIDATIONLOOKUPTYPE = "METADATA-VALIDATION_LOOKUP_TYPE"; + public static final String CONTAINER_VALIDATIONEXPRESSION = "METADATA-VALIDATION_EXPRESSION"; + public static final String CONTAINER_VALIDATIONEXTERNAL = "METADATA-VALIDATION_EXTERNAL"; + public static final String CONTAINER_VALIDATIONEXTERNALTYPE = "METADATA-VALIDATION_EXTERNAL_TYPE"; + public static final String ELEMENT_SYSTEM = "SYSTEM"; + public static final String COLUMNS = "COLUMNS"; + public static final String DATA = "DATA"; + public static final String ATTRIBUTE_RESOURCE = "Resource"; + public static final String ATTRIBUTE_CLASS = "Class"; + public static final String ATTRIBUTE_UPDATE = "Update"; + public static final String ATTRIBUTE_LOOKUP = "Lookup"; + public static final String ATTRIBUTE_VALIDATIONEXTERNAL = "ValidationExternal"; + public static final String ATTRIBUTE_VALIDATIONLOOKUP = "ValidationLookup"; + private static final Log LOG = LogFactory.getLog(JDomCompactBuilder.class); + + @Override + public Metadata doBuild(Object src) throws MetadataException { + return build((Document) src); + } + + public Metadata build(InputSource source) throws MetadataException { + SAXReader builder = new SAXReader(); + Document document; + try { + document = builder.read(source); + } catch (DocumentException e) { + throw new MetadataException("Couldn't build document", e); + } + return build(document); + } + + @Override + public MetaObject[] parse(Object src) throws MetadataException { + return parse((Document) src); + } + + public MetaObject[] parse(Document src) throws MetadataException { + Element root = src.getRootElement(); + if (!root.getName().equals(CONTAINER_ROOT)) { + throw new MetadataException("Invalid root element"); + } + Element container = root.element(CONTAINER_SYSTEM); + if (container != null) { + MSystem sys = processSystem(container); + if (root.element(CONTAINER_RESOURCE) != null) { + Metadata m = new Metadata(sys); + recurseAll(m, root); + } + return new MetaObject[] { sys }; + } + container = root.element(CONTAINER_RESOURCE); + if (container != null) { + return processResource(container); + } + container = root.element(CONTAINER_CLASS); + if (container != null) { + return processClass(container); + } + container = root.element(CONTAINER_TABLE); + if (container != null) { + return processTable(container); + } + container = root.element(CONTAINER_UPDATE); + if (container != null) { + return processUpdate(container); + } + container = root.element(CONTAINER_UPDATETYPE); + if (container != null) { + return processUpdateType(container); + } + container = root.element(CONTAINER_OBJECT); + if (container != null) { + return processObject(container); + } + container = root.element(CONTAINER_SEARCHHELP); + if (container != null) { + return processSearchHelp(container); + } + container = root.element(CONTAINER_EDITMASK); + if (container != null) { + return processEditMask(container); + } + container = root.element(CONTAINER_LOOKUP); + if (container != null) { + return processLookup(container); + } + container = root.element(CONTAINER_LOOKUPTYPE); + if (container != null) { + return processLookupType(container); + } + container = root.element(CONTAINER_VALIDATIONLOOKUP); + if (container != null) { + return processValidationLookup(container); + } + container = root.element(CONTAINER_VALIDATIONLOOKUPTYPE); + if (container != null) { + return processValidationLookupType(container); + } + container = root.element(CONTAINER_VALIDATIONEXTERNAL); + if (container != null) { + return processValidationExternal(container); + } + container = root.element(CONTAINER_VALIDATIONEXTERNALTYPE); + if (container != null) { + return processValidationExternalType(container); + } + container = root.element(CONTAINER_VALIDATIONEXPRESSION); + if (container != null) { + return processValidationExpression(container); + } + return null; + } + + public Metadata build(Document src) throws MetadataException { + Element root = src.getRootElement(); + if (!root.getName().equals(CONTAINER_ROOT)) { + throw new MetadataException("Invalid root element"); + } + Element element = root.element(CONTAINER_SYSTEM); + if (element == null) { + throw new MetadataException("Missing element " + CONTAINER_SYSTEM); + } + MSystem sys = processSystem(element); + Metadata metadata; + metadata = new Metadata(sys); + recurseAll(metadata, root); + return metadata; + } + + private void recurseAll(Metadata metadata, Element root) throws MetaParseException { + attachResource(metadata, root); + attachClass(metadata, root); + attachTable(metadata, root); + attachUpdate(metadata, root); + attachUpdateType(metadata, root); + attachObject(metadata, root); + attachSearchHelp(metadata, root); + attachEditMask(metadata, root); + attachLookup(metadata, root); + attachLookupType(metadata, root); + attachValidationLookup(metadata, root); + attachValidationLookupType(metadata, root); + attachValidationExternal(metadata, root); + attachValidationExternalType(metadata, root); + attachValidationExpression(metadata, root); + } + + private void setAttributes(MetaObject obj, String[] columns, String[] data) { + int count = columns.length; + if (count > data.length) { + count = data.length; + } + for (int i = 0; i < count; i++) { + String column = columns[i]; + String datum = data[i]; + if (!datum.equals("")) { + setAttribute(obj, column, datum); + } + } + } + + private String[] getColumns(Element el) { + Element cols = el.element(COLUMNS); + return split(cols); + } + + /** do NOT use string.split() unless your prepared to deal with loss due to token boundary conditions */ + private String[] split(Element el) { + if( el == null ) return null; + final String delimiter = "\t"; + StringTokenizer tkn = new StringTokenizer(el.getText(), delimiter, true); + List list = new LinkedList(); + tkn.nextToken(); // junk the first element + String last = null; + while (tkn.hasMoreTokens()) { + String next = tkn.nextToken(); + if (next.equals(delimiter)) { + if (last == null) { + list.add(""); + } else { + last = null; + } + } else { + list.add(next); + last = next; + } + } + return (String[]) list.toArray(new String[0]); + } + + /** + * Gets an attribute that is not expected to be null (i.e. an attribute that + * MUST exist). + * + * @param element Element + * @param name Attribute name + * @return value of attribute + * @throws MetaParseException if the value is null. + */ + private String getNonNullAttribute(Element element, String name) throws MetaParseException { + String value = element.attributeValue(name); + if (value == null) { + throw new MetaParseException("Attribute '" + name + "' not found on tag " + toString(element)); + } + return value; + } + + private String toString(Element element) { + StringBuffer buffer = new StringBuffer(); + List attributes = element.attributes(); + buffer.append("'").append(element.getName()).append("'"); + buffer.append(", attributes: ").append(attributes); + return buffer.toString(); + } + + private MSystem processSystem(Element container) { + Element element = container.element(ELEMENT_SYSTEM); + MSystem system = buildSystem(); + // system metadata is such a hack. the first one here is by far my favorite + String comment = container.elementText(MSystem.COMMENTS); + String systemId = element.attributeValue(MSystem.SYSTEMID); + String systemDescription = element.attributeValue(MSystem.SYSTEMDESCRIPTION); + String version = container.attributeValue(MSystem.VERSION); + String date = container.attributeValue(MSystem.DATE); + setAttribute(system, MSystem.COMMENTS, comment); + setAttribute(system, MSystem.SYSTEMID, systemId); + setAttribute(system, MSystem.SYSTEMDESCRIPTION, systemDescription); + setAttribute(system, MSystem.VERSION, version); + setAttribute(system, MSystem.DATE, date); + return system; + } + + private void attachResource(Metadata metadata, Element root) { + MSystem system = metadata.getSystem(); + List containers = root.elements(CONTAINER_RESOURCE); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MResource[] resources = this.processResource(container); + for (int j = 0; j < resources.length; j++) { + system.addChild(MetadataType.RESOURCE, resources[j]); + } + } + } + + private MResource[] processResource(Element resourceContainer) { + String[] columns = getColumns(resourceContainer); + List rows = resourceContainer.elements(DATA); + MResource[] resources = new MResource[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MResource resource = buildResource(); + setAttributes(resource, columns, data); + resources[i] = resource; + } + return resources; + } + + private void attachClass(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_CLASS); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + String resourceId = getNonNullAttribute(container, ATTRIBUTE_RESOURCE); + MResource resource = metadata.getResource(resourceId); + MClass[] classes = processClass(container); + for (int j = 0; j < classes.length; j++) { + resource.addChild(MetadataType.CLASS, classes[j]); + } + } + } + + private MClass[] processClass(Element classContainer) throws MetaParseException { + String name = classContainer.getName(); + String resourceId = getNonNullAttribute(classContainer, ATTRIBUTE_RESOURCE); + LOG.debug("resource name: " + resourceId + " for container " + name); + String[] columns = getColumns(classContainer); + List rows = classContainer.elements(DATA); + MClass[] classes = new MClass[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MClass clazz = buildClass(); + setAttributes(clazz, columns, data); + classes[i] = clazz; + } + return classes; + } + + private void attachTable(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_TABLE); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + String resourceId = getNonNullAttribute(container, ATTRIBUTE_RESOURCE); + String className = getNonNullAttribute(container, ATTRIBUTE_CLASS); + MClass clazz = metadata.getMClass(resourceId, className); + + if (clazz == null) { + //MarketLinx Strikes!!! + LOG.warn("Found table metadata for resource class: " + resourceId + ":" + className + + " but there is no class metadata for " + resourceId + ":" + className); + continue; + } + + MTable[] fieldMetadata = processTable(container); + for (int j = 0; j < fieldMetadata.length; j++) { + clazz.addChild(MetadataType.TABLE, fieldMetadata[j]); + } + } + } + + private MTable[] processTable(Element tableContainer) { + String[] columns = getColumns(tableContainer); + List rows = tableContainer.elements(DATA); + MTable[] fieldMetadata = new MTable[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MTable mTable = buildTable(); + setAttributes(mTable, columns, data); + fieldMetadata[i] = mTable; + } + return fieldMetadata; + } + + private void attachUpdate(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_UPDATE); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MClass parent = metadata.getMClass(getNonNullAttribute(container, ATTRIBUTE_RESOURCE), getNonNullAttribute( + container, ATTRIBUTE_CLASS)); + MUpdate[] updates = processUpdate(container); + for (int j = 0; j < updates.length; j++) { + parent.addChild(MetadataType.UPDATE, updates[j]); + } + } + } + + private MUpdate[] processUpdate(Element container) { + String[] columns = getColumns(container); + List rows = container.elements(DATA); + MUpdate[] updates = new MUpdate[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MUpdate update = buildUpdate(); + setAttributes(update, columns, data); + updates[i] = update; + } + return updates; + } + + private void attachUpdateType(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_UPDATETYPE); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MUpdate parent = metadata.getUpdate(getNonNullAttribute(container, ATTRIBUTE_RESOURCE), + getNonNullAttribute(container, ATTRIBUTE_CLASS), getNonNullAttribute(container, ATTRIBUTE_UPDATE)); + MUpdateType[] updateTypes = processUpdateType(container); + for (int j = 0; j < updateTypes.length; j++) { + parent.addChild(MetadataType.UPDATE_TYPE, updateTypes[j]); + } + } + } + + private MUpdateType[] processUpdateType(Element container) { + String[] columns = getColumns(container); + List rows = container.elements(DATA); + MUpdateType[] updateTypes = new MUpdateType[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MUpdateType updateType = buildUpdateType(); + setAttributes(updateType, columns, data); + updateTypes[i] = updateType; + } + return updateTypes; + } + + private void attachObject(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_OBJECT); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MResource parent = metadata.getResource(getNonNullAttribute(container, ATTRIBUTE_RESOURCE)); + MObject[] objects = processObject(container); + for (int j = 0; j < objects.length; j++) { + parent.addChild(MetadataType.OBJECT, objects[j]); + } + } + } + + private MObject[] processObject(Element objectContainer) { + String[] columns = getColumns(objectContainer); + List rows = objectContainer.elements(DATA); + MObject[] objects = new MObject[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MObject object = buildObject(); + setAttributes(object, columns, data); + objects[i] = object; + } + return objects; + } + + private void attachSearchHelp(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_SEARCHHELP); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MResource parent = metadata.getResource(getNonNullAttribute(container, ATTRIBUTE_RESOURCE)); + MSearchHelp[] searchHelps = processSearchHelp(container); + for (int j = 0; j < searchHelps.length; j++) { + parent.addChild(MetadataType.SEARCH_HELP, searchHelps[j]); + } + } + } + + private MSearchHelp[] processSearchHelp(Element container) { + String[] columns = getColumns(container); + List rows = container.elements(DATA); + MSearchHelp[] searchHelps = new MSearchHelp[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MSearchHelp searchHelp = buildSearchHelp(); + setAttributes(searchHelp, columns, data); + searchHelps[i] = searchHelp; + } + return searchHelps; + } + + private void attachEditMask(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_EDITMASK); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MResource parent = metadata.getResource(getNonNullAttribute(container, ATTRIBUTE_RESOURCE)); + MEditMask[] editMasks = processEditMask(container); + for (int j = 0; j < editMasks.length; j++) { + parent.addChild(MetadataType.EDITMASK, editMasks[j]); + } + } + } + + private MEditMask[] processEditMask(Element container) { + String[] columns = getColumns(container); + List rows = container.elements(DATA); + MEditMask[] editMasks = new MEditMask[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MEditMask editMask = buildEditMask(); + setAttributes(editMask, columns, data); + editMasks[i] = editMask; + } + return editMasks; + } + + private void attachLookup(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_LOOKUP); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MResource parent = metadata.getResource(getNonNullAttribute(container, ATTRIBUTE_RESOURCE)); + MLookup[] lookups = processLookup(container); + for (int j = 0; j < lookups.length; j++) { + parent.addChild(MetadataType.LOOKUP, lookups[j]); + } + } + } + + private MLookup[] processLookup(Element container) { + String[] columns = getColumns(container); + List rows = container.elements(DATA); + MLookup[] lookups = new MLookup[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MLookup lookup = buildLookup(); + setAttributes(lookup, columns, data); + lookups[i] = lookup; + } + return lookups; + } + + private void attachLookupType(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_LOOKUPTYPE); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MLookup parent = metadata.getLookup(getNonNullAttribute(container, ATTRIBUTE_RESOURCE), + getNonNullAttribute(container, ATTRIBUTE_LOOKUP)); + + if (parent == null) { + LOG.warn("Skipping lookup type: could not find lookup for tag " + toString(container)); + continue; + } + + MLookupType[] lookupTypes = processLookupType(container); + for (int j = 0; j < lookupTypes.length; j++) { + parent.addChild(MetadataType.LOOKUP_TYPE, lookupTypes[j]); + } + } + } + + private MLookupType[] processLookupType(Element container) { + String[] columns = getColumns(container); + List rows = container.elements(DATA); + MLookupType[] lookupTypes = new MLookupType[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MLookupType lookupType = buildLookupType(); + setAttributes(lookupType, columns, data); + lookupTypes[i] = lookupType; + } + return lookupTypes; + } + + private void attachValidationLookup(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_VALIDATIONLOOKUP); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MResource parent = metadata.getResource(getNonNullAttribute(container, ATTRIBUTE_RESOURCE)); + MValidationLookup[] validationLookups = processValidationLookup(container); + for (int j = 0; j < validationLookups.length; j++) { + parent.addChild(MetadataType.VALIDATION_LOOKUP, validationLookups[j]); + } + } + } + + private MValidationLookup[] processValidationLookup(Element container) { + String[] columns = getColumns(container); + List rows = container.elements(DATA); + MValidationLookup[] validationLookups = new MValidationLookup[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MValidationLookup validationLookup = buildValidationLookup(); + setAttributes(validationLookup, columns, data); + validationLookups[i] = validationLookup; + } + return validationLookups; + } + + private void attachValidationLookupType(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_VALIDATIONLOOKUPTYPE); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MValidationLookup parent = metadata.getValidationLookup(getNonNullAttribute(container, ATTRIBUTE_RESOURCE), + getNonNullAttribute(container, ATTRIBUTE_VALIDATIONLOOKUP)); + MValidationLookupType[] validationLookupTypes = processValidationLookupType(container); + for (int j = 0; j < validationLookupTypes.length; j++) { + parent.addChild(MetadataType.VALIDATION_LOOKUP_TYPE, validationLookupTypes[j]); + } + } + } + + private MValidationLookupType[] processValidationLookupType(Element container) { + String[] columns = getColumns(container); + List rows = container.elements(DATA); + MValidationLookupType[] validationLookupTypes = new MValidationLookupType[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MValidationLookupType validationLookupType = buildValidationLookupType(); + setAttributes(validationLookupType, columns, data); + validationLookupTypes[i] = validationLookupType; + } + return validationLookupTypes; + } + + private void attachValidationExternal(Metadata metadata, Element root) { + List containers = root.elements(CONTAINER_VALIDATIONEXTERNAL); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MResource parent = metadata.getResource(container.attributeValue(ATTRIBUTE_RESOURCE)); + MValidationExternal[] validationExternals = processValidationExternal(container); + for (int j = 0; j < validationExternals.length; j++) { + parent.addChild(MetadataType.VALIDATION_EXTERNAL, validationExternals[j]); + } + } + } + + private MValidationExternal[] processValidationExternal(Element container) { + String[] columns = getColumns(container); + List rows = container.elements(DATA); + MValidationExternal[] validationExternals = new MValidationExternal[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MValidationExternal validationExternal = buildValidationExternal(); + setAttributes(validationExternal, columns, data); + validationExternals[i] = validationExternal; + } + return validationExternals; + } + + private void attachValidationExternalType(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_VALIDATIONEXTERNALTYPE); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MValidationExternal parent = metadata.getValidationExternal(getNonNullAttribute(container, + ATTRIBUTE_RESOURCE), getNonNullAttribute(container, ATTRIBUTE_VALIDATIONEXTERNAL)); + MValidationExternalType[] validationExternalTypes = processValidationExternalType(container); + for (int j = 0; j < validationExternalTypes.length; j++) { + parent.addChild(MetadataType.VALIDATION_EXTERNAL_TYPE, validationExternalTypes[j]); + } + } + } + + private MValidationExternalType[] processValidationExternalType(Element container) { + String[] columns = getColumns(container); + List rows = container.elements(DATA); + MValidationExternalType[] validationExternalTypes = new MValidationExternalType[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MValidationExternalType validationExternalType = buildValidationExternalType(); + setAttributes(validationExternalType, columns, data); + validationExternalTypes[i] = validationExternalType; + } + return validationExternalTypes; + } + + private void attachValidationExpression(Metadata metadata, Element root) throws MetaParseException { + List containers = root.elements(CONTAINER_VALIDATIONEXPRESSION); + for (int i = 0; i < containers.size(); i++) { + Element container = (Element) containers.get(i); + MResource parent = metadata.getResource(getNonNullAttribute(container, ATTRIBUTE_RESOURCE)); + MValidationExpression[] expressions = processValidationExpression(container); + for (int j = 0; j < expressions.length; j++) { + parent.addChild(MetadataType.VALIDATION_EXPRESSION, expressions[j]); + } + } + } + + private MValidationExpression[] processValidationExpression(Element container) { + String[] columns = getColumns(container); + List rows = container.elements(DATA); + MValidationExpression[] expressions = new MValidationExpression[rows.size()]; + for (int i = 0; i < expressions.length; i++) { + Element element = (Element) rows.get(i); + String[] data = split(element); + MValidationExpression expression = buildValidationExpression(); + setAttributes(expression, columns, data); + expressions[i] = expression; + } + return expressions; + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/JDomStandardBuilder.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/JDomStandardBuilder.java new file mode 100644 index 0000000..7fd0d3c --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/JDomStandardBuilder.java @@ -0,0 +1,628 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +import com.ossez.usreio.tests.common.metadata.types.MClass; +import com.ossez.usreio.tests.common.metadata.types.MEditMask; +import com.ossez.usreio.tests.common.metadata.types.MForeignKey; +import com.ossez.usreio.tests.common.metadata.types.MLookup; +import com.ossez.usreio.tests.common.metadata.types.MLookupType; +import com.ossez.usreio.tests.common.metadata.types.MObject; +import com.ossez.usreio.tests.common.metadata.types.MResource; +import com.ossez.usreio.tests.common.metadata.types.MSearchHelp; +import com.ossez.usreio.tests.common.metadata.types.MSystem; +import com.ossez.usreio.tests.common.metadata.types.MTable; +import com.ossez.usreio.tests.common.metadata.types.MUpdate; +import com.ossez.usreio.tests.common.metadata.types.MUpdateHelp; +import com.ossez.usreio.tests.common.metadata.types.MUpdateType; +import com.ossez.usreio.tests.common.metadata.types.MValidationExpression; +import com.ossez.usreio.tests.common.metadata.types.MValidationExternal; +import com.ossez.usreio.tests.common.metadata.types.MValidationExternalType; +import com.ossez.usreio.tests.common.metadata.types.MValidationLookup; +import com.ossez.usreio.tests.common.metadata.types.MValidationLookupType; +import org.dom4j.Attribute; +import org.dom4j.Document; +import org.dom4j.Element; + +/** Parses apart a complete Standard-XML response, returns a Metadata object */ +public class JDomStandardBuilder extends MetadataBuilder { + public static final String ELEMENT_SYSTEM = "System"; + public static final String ELEMENT_RESOURCE = "Resource"; + public static final String ELEMENT_FOREIGNKEY = "ForeignKey"; + public static final String ELEMENT_CLASS = "Class"; + public static final String ELEMENT_TABLE = "Field"; + public static final String ELEMENT_UPDATE = "UpdateType"; + public static final String ELEMENT_UPDATETYPE = "UpdateField"; + public static final String ELEMENT_OBJECT = "Object"; + public static final String ELEMENT_SEARCHHELP = "SearchHelp"; + public static final String ELEMENT_EDITMASK = "EditMask"; + public static final String ELEMENT_UPDATEHELP = "UpdateHelp"; + public static final String ELEMENT_LOOKUP = "Lookup"; + public static final String ELEMENT_LOOKUPTYPE = "LookupType"; + public static final String ELEMENT_VALIDATIONLOOKUP = "ValidationLookup"; + public static final String ELEMENT_VALIDATIONLOOKUPTYPE = "ValidationLookupType"; + public static final String ELEMENT_VALIDATIONEXPRESSION = "ValidationExpression"; + public static final String ELEMENT_VALIDATIONEXTERNAL = "ValidationExternalType"; + public static final String ELEMENT_VALIDATIONEXTERNALTYPE = "ValidationExternal"; + public static final String ATTRIBUTE_RESOURCEID = ELEMENT_RESOURCE; + public static final String ATTRIBUTE_CLASSNAME = ELEMENT_CLASS; + public static final String ATTRIBUTE_UPDATE = ELEMENT_UPDATE; + public static final String ATTRIBUTE_LOOKUP = ELEMENT_LOOKUP; + public static final String ATTRIBUTE_VALIDATIONLOOKUP = ELEMENT_VALIDATIONLOOKUP; + public static final String ATTRIBUTE_VALIDATIONEXTERNAL = ELEMENT_VALIDATIONEXTERNAL; + public static final Map sType2Element = new HashMap(); + + static { + sType2Element.put(MetadataType.SYSTEM, ELEMENT_SYSTEM); + sType2Element.put(MetadataType.RESOURCE, ELEMENT_RESOURCE); + sType2Element.put(MetadataType.FOREIGNKEYS, ELEMENT_FOREIGNKEY); + sType2Element.put(MetadataType.CLASS, ELEMENT_CLASS); + sType2Element.put(MetadataType.TABLE, ELEMENT_TABLE); + sType2Element.put(MetadataType.UPDATE, ELEMENT_UPDATE); + sType2Element.put(MetadataType.UPDATE_TYPE, ELEMENT_UPDATETYPE); + sType2Element.put(MetadataType.SEARCH_HELP, ELEMENT_SEARCHHELP); + sType2Element.put(MetadataType.EDITMASK, ELEMENT_EDITMASK); + sType2Element.put(MetadataType.UPDATE_HELP, ELEMENT_UPDATEHELP); + sType2Element.put(MetadataType.LOOKUP, ELEMENT_LOOKUP); + sType2Element.put(MetadataType.LOOKUP_TYPE, ELEMENT_LOOKUPTYPE); + sType2Element.put(MetadataType.VALIDATION_LOOKUP, ELEMENT_VALIDATIONLOOKUP); + sType2Element.put(MetadataType.VALIDATION_LOOKUP_TYPE, ELEMENT_VALIDATIONLOOKUPTYPE); + sType2Element.put(MetadataType.VALIDATION_EXTERNAL, ELEMENT_VALIDATIONEXTERNAL); + sType2Element.put(MetadataType.VALIDATION_EXTERNAL_TYPE, ELEMENT_VALIDATIONEXTERNALTYPE); + sType2Element.put(MetadataType.VALIDATION_EXPRESSION, ELEMENT_VALIDATIONEXPRESSION); + } + + @Override + public Metadata doBuild(Object src) throws MetadataException { + return build((Document) src); + } + + public Metadata build(Document src) throws MetadataException { + Element element = src.getRootElement(); + expectElement(element, CONTAINER_ROOT); + element = getElement(element, CONTAINER_METADATA); + return build(element); + } + + @Override + public MetaObject[] parse(Object src) throws MetadataException { + return parse((Document) src); + } + + public MetaObject[] parse(Document src) throws MetadataException { + Element element = src.getRootElement(); + expectElement(element, CONTAINER_ROOT); + Element container = getElement(element, CONTAINER_METADATA); + boolean recurse = checkForRecursion(container); + List list = container.elements(); + if (list.size() == 0) { + return null; + } + return processContainer(null, (Element) list.get(0), recurse); + } + + /** + * Function to determine if a request contains recursive data or not. + * This is done here instead of inside processContainer because, well, + * it's easier and more reliable (processContainer might not figure out + * that a request is recursive until the third or 4th child if there are + * no children for the first couple of elements. + * + * @param top The outside METADATA container. + * @return true if the request is recursive + * + */ + private boolean checkForRecursion(Element top) { + /* + * this seems like a really nasty loop. However, if there are a + * lot of recursive elements, we'll find out pretty quickly, and if + * we fall all the way to the end then there probably wasn't that + * much to look through. + */ + Iterator children = top.elements().iterator(); + while (children.hasNext()) { + /* each of these is a container (METADATA-*) type */ + Element element = (Element) children.next(); + Iterator iterator = element.elements().iterator(); + while (iterator.hasNext()) { + /* each of these is an item element */ + Element child = (Element) iterator.next(); + Iterator subtypes = child.elements().iterator(); + while (subtypes.hasNext()) { + Element subtype = (Element) subtypes.next(); + if (subtype.getName().startsWith(CONTAINER_PREFIX)) { + return true; + } + } + } + } + return false; + } + + private MetaObject[] processContainer(MetaObject parent, Element container, boolean recursion) { + MetadataType type = (MetadataType) sContainer2Type.get(container.getName()); + if (type == null) { + throw new RuntimeException("no matching type for container " + container.getName()); + } + List elements = container.elements((String) sType2Element.get(type)); + String path = getPath(container); + List output = null; + if (parent == null) { + output = new LinkedList(); + } + for (int i = 0; i < elements.size(); i++) { + Element element = (Element) elements.get(i); + MetaObject obj = newType(type); + setAttributes(obj, element); + if (output != null) { + output.add(obj); + } + if (parent != null) { + parent.addChild(type, obj); + } else { + /** + * Weirdness abounds. There IS an ID attribute of System, + * and the SystemID is included in the Metadata container + * attributes, but the system id is not part of the metadata + * request path for a getMetadata request, so we ignore it. + */ + if (!type.equals(MetadataType.SYSTEM)) { + obj.setPath(path); + } + } + if (recursion) { + MetadataType[] childTypes = obj.getChildTypes(); + for (int j = 0; j < childTypes.length; j++) { + MetadataType childType = childTypes[j]; + Element childContainer = element.element(CONTAINER_PREFIX + childType.name()); + if (childContainer == null) { + obj.addChild(childType, null); + } else { + processContainer(obj, childContainer, true); + } + } + } + } + if (output == null) { + return null; + } + return (MetaObject[]) output.toArray(new MetaObject[0]); + } + + String getPath(Element container) { + String resource = container.attributeValue(ATTRIBUTE_RESOURCEID); + if (resource == null) { + return null; + } + String classname = container.attributeValue(ATTRIBUTE_CLASSNAME); + if (classname != null) { + String update = container.attributeValue(ATTRIBUTE_UPDATE); + if (update != null) { + return resource + ":" + classname + ":" + update; + } + return resource + ":" + classname; + } + String lookup = container.attributeValue(ATTRIBUTE_LOOKUP); + if (lookup != null) { + return resource + ":" + lookup; + } + String vallkp = container.attributeValue(ATTRIBUTE_VALIDATIONLOOKUP); + if (vallkp != null) { + return resource + ":" + vallkp; + } + String vale = container.attributeValue(ATTRIBUTE_VALIDATIONEXTERNAL); + if (vale != null) { + return resource + ":" + vale; + } + return resource; + } + + public Metadata build(Element element) throws MetadataException { + expectElement(element, CONTAINER_METADATA); + element = getElement(element, CONTAINER_SYSTEM); + //maybe i get the attribute here + MSystem sys = processSystem(element); + return new Metadata(sys); + } + + private Element getElement(Element parent, String type) throws MetadataException { + Element element = parent.element(type); + if (element == null) { + throw new MetadataException("Missing element " + type); + } + return element; + } + + + private void expectElement(Element element, String type) throws MetadataException { + if (!element.getName().equalsIgnoreCase(type)) {// changed to ignore case + throw new MetadataException("Expecting element " + type + ", got " + element.getName()); + } + } + + private void setAttributes(MetaObject obj, Element el) { + + List children = el.elements(); + for (int i = 0; i < children.size(); i++) { + Element child = (Element) children.get(i); + String name = child.getName(); + if (!name.startsWith(CONTAINER_PREFIX)) { + String value = child.getTextTrim(); + setAttribute(obj, name, value); + } else { + // LOG.info("skipping container element " + name); + } + } + } + + //when atrributes from the xml element are needed + public void setAttributesFromXMLAttr(MetaObject obj, Element el) { + + Iterator attrIter = el.getParent().attributes().iterator(); + + while(attrIter.hasNext()){ + Attribute attr = (Attribute) attrIter.next(); + String name = attr.getName(); + String value= attr.getValue().trim(); + setAttribute(obj, name, value); + } + } + + + /** + * If we're a recursive request, initialize all possible child types so + * we don't have to try to pull them later, dynamically + */ + private void init(MetaObject item) { + MetadataType[] childTypes = item.getChildTypes(); + for (int i = 0; i < childTypes.length; i++) { + MetadataType type = childTypes[i]; + item.addChild(type, null); + } + } + + private MSystem processSystem(Element container) { + Element element = container.element(ELEMENT_SYSTEM); + if (element == null){ + element = container.element(ELEMENT_SYSTEM.toUpperCase()); + } + MSystem system = buildSystem(); + init(system); + setAttributesFromXMLAttr(system, element); + setAttributes(system, element); + Element child; + child = element.element(CONTAINER_RESOURCE); + if (child != null) { + processResource(system, child); + } + child = element.element(CONTAINER_FOREIGNKEY); + if (child != null) { + processForeignKey(system, child); + } + return system; + } + + private void processResource(MSystem system, Element container) { + List resources = container.elements(ELEMENT_RESOURCE); + for (int i = 0; i < resources.size(); i++) { + Element element = (Element) resources.get(i); + MResource resource = buildResource(); + init(resource); + setAttributes(resource, element); + system.addChild(MetadataType.RESOURCE, resource); + Element child; + child = element.element(CONTAINER_CLASS); + if (child != null) { + processClass(resource, child); + } + child = element.element(CONTAINER_OBJECT); + if (child != null) { + processObject(resource, child); + } + child = element.element(CONTAINER_SEARCH_HELP); + if (child != null) { + processSearchHelp(resource, child); + } + child = element.element(CONTAINER_EDITMASK); + if (child != null) { + processEditMask(resource, child); + } + child = element.element(CONTAINER_LOOKUP); + if (child != null) { + processLookup(resource, child); + } + child = element.element(CONTAINER_UPDATEHELP); + if (child != null) { + processUpdateHelp(resource, child); + } + child = element.element(CONTAINER_VALIDATIONLOOKUP); + if (child != null) { + processValidationLookup(resource, child); + } + child = element.element(CONTAINER_VALIDATIONEXPRESSION); + if (child != null) { + processValidationExpression(resource, child); + } + child = element.element(CONTAINER_VALIDATIONEXTERNAL); + if (child != null) { + processValidationExternal(resource, child); + } + } + } + + private void processEditMask(MResource parent, Element container) { + List elements = container.elements(ELEMENT_EDITMASK); + for (int i = 0; i < elements.size(); i++) { + Element element = (Element) elements.get(i); + MEditMask mask = buildEditMask(); + setAttributes(mask, element); + parent.addChild(MetadataType.EDITMASK, mask); + } + } + + private void processLookup(MResource parent, Element container) { + List elements15 = container.elements(ELEMENT_LOOKUP); + List elements17 = container.elements(ELEMENT_LOOKUPTYPE); + List elements; + //some Rets Servers have lookuptype and lookup elements interchanged + if (elements15.isEmpty()){ + elements = elements17; + } else { + elements = elements15; + } + for (int i = 0; i < elements.size(); i++) { + Element element = (Element) elements.get(i); + MLookup lookup = buildLookup(); + init(lookup); + setAttributes(lookup, element); + parent.addChild(MetadataType.LOOKUP, lookup); + Element child = element.element(CONTAINER_LOOKUPTYPE); + if (child != null) { + processLookupType(lookup, child); + } + } + } + + private void processLookupType(MLookup parent, Element container) { + + List elements15 = container.elements(ELEMENT_LOOKUPTYPE);// check spec + List elements17 = container.elements(ELEMENT_LOOKUP); + List elements; + //some Rets Servers have lookuptype and lookup elements interchanged + if (elements15.isEmpty()){ + elements = elements17; + } else { + elements = elements15; + } + for (int i = 0; i < elements.size(); i++) { + Element element = (Element) elements.get(i); + MLookupType type = buildLookupType(); + setAttributes(type, element); + parent.addChild(MetadataType.LOOKUP_TYPE, type); + } + } + + private void processUpdateHelp(MResource parent, Element container) { + List elements = container.elements(ELEMENT_UPDATEHELP); + for (int i = 0; i < elements.size(); i++) { + Element element = (Element) elements.get(i); + MUpdateHelp help = buildUpdateHelp(); + setAttributes(help, element); + parent.addChild(MetadataType.UPDATE_HELP, help); + } + } + + private void processValidationLookup(MResource parent, Element container) { + List elements = container.elements(ELEMENT_VALIDATIONLOOKUP); + for (int i = 0; i < elements.size(); i++) { + Element element = (Element) elements.get(i); + MValidationLookup lookup = buildValidationLookup(); + init(lookup); + setAttributes(lookup, element); + parent.addChild(MetadataType.VALIDATION_LOOKUP, lookup); + Element child = element.element(CONTAINER_VALIDATIONLOOKUPTYPE); + if (child != null) { + processValidationLookupType(lookup, child); + } + } + } + + private void processValidationLookupType(MValidationLookup parent, Element container) { + List elements = container.elements(ELEMENT_VALIDATIONLOOKUPTYPE); + for (int i = 0; i < elements.size(); i++) { + Element element = (Element) elements.get(i); + MValidationLookupType lookupType = buildValidationLookupType(); + setAttributes(lookupType, element); + parent.addChild(MetadataType.VALIDATION_LOOKUP_TYPE, lookupType); + } + } + + private void processValidationExpression(MResource parent, Element container) { + List elements = container.elements(ELEMENT_VALIDATIONEXPRESSION); + for (int i = 0; i < elements.size(); i++) { + Element element = (Element) elements.get(i); + MValidationExpression expression = buildValidationExpression(); + setAttributes(expression, element); + parent.addChild(MetadataType.VALIDATION_EXPRESSION, expression); + } + } + + private void processValidationExternal(MResource parent, Element container) { + List elements = container.elements(ELEMENT_VALIDATIONEXTERNAL); + for (int i = 0; i < elements.size(); i++) { + Element element = (Element) elements.get(i); + MValidationExternal external = buildValidationExternal(); + init(external); + setAttributes(external, element); + parent.addChild(MetadataType.VALIDATION_EXTERNAL, external); + Element child = element.element(CONTAINER_VALIDATIONEXTERNALTYPE); + if (child != null) { + processValidationExternalType(external, child); + } + } + } + + private void processValidationExternalType(MValidationExternal parent, Element container) { + List elements = container.elements(ELEMENT_VALIDATIONEXTERNALTYPE); + for (int i = 0; i < elements.size(); i++) { + Element element = (Element) elements.get(i); + MValidationExternalType type = buildValidationExternalType(); + setAttributes(type, element); + parent.addChild(MetadataType.VALIDATION_EXTERNAL_TYPE, type); + } + } + + private void processSearchHelp(MResource parent, Element container) { + List searchhelps = container.elements(ELEMENT_SEARCHHELP); + for (int i = 0; i < searchhelps.size(); i++) { + Element element = (Element) searchhelps.get(i); + MSearchHelp searchhelp = buildSearchHelp(); + setAttributes(searchhelp, element); + parent.addChild(MetadataType.SEARCH_HELP, searchhelp); + } + } + + private void processObject(MResource parent, Element container) { + List objects = container.elements(ELEMENT_OBJECT); + for (int i = 0; i < objects.size(); i++) { + Element element = (Element) objects.get(i); + MObject obj = buildObject(); + setAttributes(obj, element); + parent.addChild(MetadataType.OBJECT, obj); + } + } + + private void processClass(MResource parent, Element container) { + List classes = container.elements(ELEMENT_CLASS); + for (int i = 0; i < classes.size(); i++) { + Element element = (Element) classes.get(i); + MClass clazz = buildClass(); + init(clazz); + setAttributes(clazz, element); + parent.addChild(MetadataType.CLASS, clazz); + Element child; + child = element.element(CONTAINER_TABLE); + if (child != null) { + processTable(clazz, child); + } + child = element.element(CONTAINER_UPDATE); + if (child != null) { + processUpdate(clazz, child); + } + } + } + + private void processTable(MClass parent, Element container) { + List tables = container.elements(ELEMENT_TABLE); + for (int i = 0; i < tables.size(); i++) { + Element element = (Element) tables.get(i); + MTable table = buildTable(); + setAttributes(table, element); + parent.addChild(MetadataType.TABLE, table); + } + } + + private void processUpdate(MClass parent, Element container) { + List updates = container.elements(ELEMENT_UPDATE); + for (int i = 0; i < updates.size(); i++) { + Element element = (Element) updates.get(i); + MUpdate update = buildUpdate(); + init(update); + setAttributes(update, element); + parent.addChild(MetadataType.UPDATE, update); + Element child = element.element(CONTAINER_UPDATE_TYPE); + if (child != null) { + processUpdateType(update, child); + } + } + } + + private void processUpdateType(MUpdate parent, Element container) { + List updateFields = container.elements(ELEMENT_UPDATETYPE); + for (int i = 0; i < updateFields.size(); i++) { + Element element = (Element) updateFields.get(i); + MUpdateType updateType = buildUpdateType(); + parent.addChild(MetadataType.UPDATE_TYPE, updateType); + setAttributes(updateType, element); + } + } + + private void processForeignKey(MSystem system, Element container) { + List fkeys = container.elements("ForeignKey"); + for (int i = 0; i < fkeys.size(); i++) { + Element element = (Element) fkeys.get(i); + MForeignKey foreignKey = buildForeignKey(); + setAttributes(foreignKey, element); + system.addChild(MetadataType.FOREIGNKEYS, foreignKey); + } + } + + public static final String CONTAINER_PREFIX = "METADATA-"; + + public static final String CONTAINER_ROOT = "RETS"; + + public static final String CONTAINER_METADATA = "METADATA"; + + public static final String CONTAINER_SYSTEM = "METADATA-SYSTEM"; + + public static final String CONTAINER_RESOURCE = "METADATA-RESOURCE"; + + public static final String CONTAINER_FOREIGNKEY = "METADATA-FOREIGN_KEYS"; + + public static final String CONTAINER_CLASS = "METADATA-CLASS"; + + public static final String CONTAINER_TABLE = "METADATA-TABLE"; + + public static final String CONTAINER_UPDATE = "METADATA-UPDATE"; + + public static final String CONTAINER_UPDATE_TYPE = "METADATA-UPDATE_TYPE"; + + public static final String CONTAINER_OBJECT = "METADATA-OBJECT"; + + public static final String CONTAINER_SEARCH_HELP = "METADATA-SEARCH_HELP"; + + public static final String CONTAINER_EDITMASK = "METADATA-EDITMASK"; + + public static final String CONTAINER_UPDATEHELP = "METADATA-UPDATE_HELP"; + + public static final String CONTAINER_LOOKUP = "METADATA-LOOKUP"; + + public static final String CONTAINER_LOOKUPTYPE = "METADATA-LOOKUP_TYPE"; + + public static final String CONTAINER_VALIDATIONLOOKUP = "METADATA-VALIDATION_LOOKUP"; + + public static final String CONTAINER_VALIDATIONLOOKUPTYPE = "METADATA-VALIDATION_LOOKUP_TYPE"; + + public static final String CONTAINER_VALIDATIONEXPRESSION = "METADATA-VALIDATION_EXPRESSION"; + + public static final String CONTAINER_VALIDATIONEXTERNAL = "METADATA-VALIDATION_EXTERNAL"; + + public static final String CONTAINER_VALIDATIONEXTERNALTYPE = "METADATA-VALIDATION_EXTERNAL_TYPE"; + + public static final Map sContainer2Type = new HashMap(); + static { + for (int i = 0; i < MetadataType.values().length; i++) { + MetadataType type = MetadataType.values()[i]; + sContainer2Type.put(CONTAINER_PREFIX + type.name(), type); + } + /* you have got to be kidding me. The spec (compact) says + METADATA-FOREIGNKEYS and that's the request type but the DTD says + METADATA-FOREIGN_KEY. + I think I'm going to be sick. + */ + sContainer2Type.remove(CONTAINER_PREFIX + MetadataType.FOREIGNKEYS.name()); + sContainer2Type.put(CONTAINER_FOREIGNKEY, MetadataType.FOREIGNKEYS); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetaCollector.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetaCollector.java new file mode 100644 index 0000000..18ac1a8 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetaCollector.java @@ -0,0 +1,20 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata; + +import java.io.Serializable; + +/** Interface for Metadata objects to collect their children. */ +public interface MetaCollector extends Serializable { + /** + * @param path path to the parent object. + */ + public MetaObject[] getMetadata(MetadataType type, String path) throws MetadataException; + + public MetaObject[] getMetadataRecursive(MetadataType type, String path) throws MetadataException; +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetaObject.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetaObject.java new file mode 100644 index 0000000..6e30a2d --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetaObject.java @@ -0,0 +1,366 @@ +package com.ossez.usreio.tests.common.metadata; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +//import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import com.ossez.usreio.common.util.CaseInsensitiveTreeMap; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import com.ossez.usreio.tests.common.metadata.attrib.AttrAlphanum; +import com.ossez.usreio.tests.common.metadata.attrib.AttrBoolean; +import com.ossez.usreio.tests.common.metadata.attrib.AttrDate; +import com.ossez.usreio.tests.common.metadata.attrib.AttrNumeric; +import com.ossez.usreio.tests.common.metadata.attrib.AttrNumericPositive; +import com.ossez.usreio.tests.common.metadata.attrib.AttrPlaintext; +import com.ossez.usreio.tests.common.metadata.attrib.AttrText; +import com.ossez.usreio.tests.common.metadata.attrib.AttrVersion; + +public abstract class MetaObject implements Serializable { + private static final Log LOG = LogFactory.getLog(MetaObject.class); + + /** a standard parser used by different child types */ + protected static final AttrType sAlphanum = new AttrAlphanum(0, 0); + protected static final AttrType sAlphanum64 = new AttrAlphanum(1, 64); + protected static final AttrType sAlphanum32 = new AttrAlphanum(1, 32); + protected static final AttrType sAlphanum24 = new AttrAlphanum(1, 24); + protected static final AttrType sAlphanum10 = new AttrAlphanum(1, 10); + protected static final AttrType sPlaintext = new AttrPlaintext(0, 0); + protected static final AttrType sPlaintext1024 = new AttrPlaintext(1, 1024); + protected static final AttrType sPlaintext512 = new AttrPlaintext(1, 512); + protected static final AttrType sPlaintext128 = new AttrPlaintext(1, 128); + protected static final AttrType sPlaintext64 = new AttrPlaintext(1, 64); + protected static final AttrType sPlaintext32 = new AttrPlaintext(1, 32); + protected static final AttrType sText = new AttrText(0, 0); + protected static final AttrType sText1024 = new AttrText(1, 1024); + protected static final AttrType sText512 = new AttrText(1, 512); + protected static final AttrType sText256 = new AttrText(1, 256); + protected static final AttrType sText128 = new AttrText(1, 128); + protected static final AttrType sText64 = new AttrText(1, 64); + protected static final AttrType sText32 = new AttrText(1, 32); + protected static final AttrType sAttrBoolean = new AttrBoolean(); + protected static final AttrType sAttrDate = new AttrDate(); + protected static final AttrType sAttrNumeric = new AttrNumeric(); + protected static final AttrType sAttrNumericPositive = new AttrNumericPositive(); + protected static final AttrType sAttrVersion = new AttrVersion(); + protected static final AttrType sAttrMetadataEntryId = sAlphanum32; + protected static final MetadataType[] sNoChildren = new MetadataType[0]; + + protected static final AttrType retsid = sAlphanum32; + protected static final AttrType retsname = sAlphanum64; + + public static final boolean STRICT_PARSING = true; + public static final boolean LOOSE_PARSING = false; + public static final boolean DEFAULT_PARSING = LOOSE_PARSING; + + /** the metdata path to this object */ + protected String path; + /** map of child type to map of child id to child object */ + protected Map childTypes; + /** map of attribute name to attribute object (as parsed by attrtype) */ + protected Map attributes; + /** map of attribute name to AttrType parser */ + protected Map attrTypes; + + + private static Map sAttributeMapCache = new HashMap(); + private MetaCollector mCollector; + private boolean strict; + + public MetaObject(boolean strictParsing) { + this.strict = strictParsing; + if (strictParsing) { + this.attributes = new HashMap(); + } else { + this.attributes = new CaseInsensitiveTreeMap(); + } + this.attrTypes = this.getAttributeMap(strictParsing); + MetadataType[] types = getChildTypes(); + this.childTypes = new HashMap(); + for (int i = 0; i < types.length; i++) { + this.childTypes.put(types[i], null); + } + } + + private Map getAttributeMap(boolean strictParsing) { + synchronized (sAttributeMapCache) { + Map map = sAttributeMapCache.get(new CacheKey(this, strictParsing)); + if (map == null) { + if (strictParsing) { + map = new HashMap(); + } else { + map = new CaseInsensitiveTreeMap(); + } + addAttributesToMap(map); + // Let's make sure no one mucks with the map later + map = Collections.unmodifiableMap(map); + sAttributeMapCache.put(new CacheKey(this, strictParsing), map); + if (LOG.isDebugEnabled()) { + LOG.debug("Adding to attribute cache: " + this.getClass().getName() + ", " + strictParsing); + } + } + return map; + } + } + + public static void clearAttributeMapCache() { + synchronized (sAttributeMapCache) { + sAttributeMapCache.clear(); + } + } + + public Collection getChildren(MetadataType type) { + if (!this.childTypes.containsKey(type)) { + // throw new IllegalArgumentException? + return null; + } + Object o = this.childTypes.get(type); + if (o == null) { + if (!fetchChildren(type)) { + return Collections.EMPTY_SET; + } + o = this.childTypes.get(type); + } + if (o instanceof Map) { + Map m = (Map) o; + return m.values(); + } + return (Collection) o; + } + + private boolean fetchChildren(MetadataType type) { + this.childTypes.put(type, new HashMap()); + try { + MetaObject[] children = null; + if (this.mCollector != null) { + children = this.mCollector.getMetadata(type, getPath()); + } + if (children == null) { + return false; + } + for (int i = 0; i < children.length; i++) { + MetaObject child = children[i]; + addChild(type, child); + } + } catch (MetadataException e) { + LOG.error(toString() + " unable to fetch " + type.name() + " children"); + return false; + } + return true; + } + + public MetaObject getChild(MetadataType type, String id) { + if (id == null) { + return null; + } + try { + if (this.childTypes.get(type) == null && this.mCollector != null) { + if (!fetchChildren(type)) { + return null; + } + } + Map m = (Map) this.childTypes.get(type); + if (m == null) { + return null; + } + return (MetaObject) m.get(id); + } catch (ClassCastException e) { + return null; + } + } + + public Object getAttribute(String key) { + return this.attributes.get(key); + } + + public Set getKnownAttributes() { + return this.attrTypes.keySet(); + } + + public String getAttributeAsString(String key) { + Object value = this.attributes.get(key); + if (value == null) { + return null; + } + if (this.attrTypes.containsKey(key)) { + AttrType type = (AttrType) this.attrTypes.get(key); + return type.render(value); + } + return value.toString(); + } + + protected Object getTypedAttribute(String key, Class type) { + AttrType atype = (AttrType) this.attrTypes.get(key); + if (atype == null) { + return null; + } + if (atype.getType() == type) { + return this.attributes.get(key); + } + LOG.warn("type mismatch, expected " + type.getName() + " but" + " got " + atype.getType().getName()); + return null; + } + + public String getDateAttribute(String key) { + return (String) getTypedAttribute(key, String.class); + } + + public String getStringAttribute(String key) { + return (String) getTypedAttribute(key, String.class); + } + + public int getIntAttribute(String key) { + Integer i = (Integer) getTypedAttribute(key, Integer.class); + if (i == null) { + return 0; + } + return i.intValue(); + } + + public boolean getBooleanAttribute(String key) { + Boolean b = (Boolean) getTypedAttribute(key, Boolean.class); + if (b == null) { + return false; + } + return b.booleanValue(); + } + + public void setAttribute(String key, String value) { + if (value == null) { + // LOG.warning() + return; + } + if (this.attrTypes.containsKey(key)) { + AttrType type = (AttrType) this.attrTypes.get(key); + try { + this.attributes.put(key, type.parse(value,this.strict)); + } catch (MetaParseException e) { + LOG.warn(toString() + " couldn't parse attribute " + key + ", value " + value + ": " + e.getMessage()); + } + } else { + this.attributes.put(key, value); + LOG.warn("Unknown key (" + toString() + "): " + key); + } + } + + public void addChild(MetadataType type, MetaObject child) { + if (this.childTypes.containsKey(type)) { + Object obj = this.childTypes.get(type); + Map map; + if (obj == null) { + map = new HashMap(); + this.childTypes.put(type, map); + } else { + map = (Map) obj; + } + if (child == null) { + return; + } + String id = child.getId(); + + child.setPath(this.getPath()); + child.setCollector(this.mCollector); + if (id != null) { + map.put(id, child); + } + return; + } + } + + public String getId() { + String idAttr = getIdAttr(); + if (idAttr == null) { + /** cheap hack so everything's a damn map */ + return Integer.toString(hashCode()); + } + return getAttributeAsString(idAttr); + } + + public String getPath() { + return this.path; + } + + protected void setPath(String parent) { + if (parent == null || parent.equals("")) { + this.path = getId(); + } else { + this.path = parent + ":" + getId(); + } + } + + @Override + public String toString() { + ToStringBuilder tsb = new ToStringBuilder(this); + Iterator iter = getKnownAttributes().iterator(); + while (iter.hasNext()) { + String key = (String) iter.next(); + tsb.append(key, getAttributeAsString(key)); + } + return tsb.toString(); + } + + public void setCollector(MetaCollector c) { + this.mCollector = c; + Iterator iterator = this.childTypes.keySet().iterator(); + while (iterator.hasNext()) { + MetadataType type = (MetadataType) iterator.next(); + Map map = (Map) this.childTypes.get(type); + if (map == null) { + continue; + } + Collection children = map.values(); + for (Iterator iter = children.iterator(); iter.hasNext();) { + MetaObject object = (MetaObject) iter.next(); + object.setCollector(c); + } + } + } + + public abstract MetadataType[] getChildTypes(); + + protected abstract String getIdAttr(); + + /** + * Adds attributes to an attribute map. This is called by the MetaObject + * constructor to initialize a map of atributes. This map may be cached, + * so this method may not be called for every object construction. + * + * @param attributeMap Map to add attributes to + */ + protected abstract void addAttributesToMap(Map attributeMap); + +} + +class CacheKey { + private Class mClass; + private boolean strictParsing; + + public CacheKey(MetaObject metaObject, boolean strictParsing) { + this.mClass = metaObject.getClass(); + this.strictParsing = strictParsing; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof CacheKey)) { + return false; + } + CacheKey rhs = (CacheKey) obj; + return new EqualsBuilder().append(this.mClass, rhs.mClass).append(this.strictParsing, rhs.strictParsing).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(this.mClass).append(this.strictParsing).toHashCode(); + } + +} + diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetaParseException.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetaParseException.java new file mode 100644 index 0000000..aa85f15 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetaParseException.java @@ -0,0 +1,26 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata; + +public class MetaParseException extends MetadataException { + public MetaParseException() { + super(); + } + + public MetaParseException(String msg) { + super(msg); + } + + public MetaParseException(Throwable cause) { + super(cause); + } + + public MetaParseException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/Metadata.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/Metadata.java new file mode 100644 index 0000000..2775002 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/Metadata.java @@ -0,0 +1,154 @@ +package com.ossez.usreio.tests.common.metadata; + +import java.io.Serializable; + +import com.ossez.usreio.tests.common.metadata.types.MSystem; +import com.ossez.usreio.tests.common.metadata.types.MResource; +import com.ossez.usreio.tests.common.metadata.types.MForeignKey; +import com.ossez.usreio.tests.common.metadata.types.MClass; +import com.ossez.usreio.tests.common.metadata.types.MTable; +import com.ossez.usreio.tests.common.metadata.types.MUpdate; +import com.ossez.usreio.tests.common.metadata.types.MUpdateType; +import com.ossez.usreio.tests.common.metadata.types.MObject; +import com.ossez.usreio.tests.common.metadata.types.MValidationExternal; +import com.ossez.usreio.tests.common.metadata.types.MValidationLookup; +import com.ossez.usreio.tests.common.metadata.types.MLookup; +import com.ossez.usreio.tests.common.metadata.types.MSearchHelp; + +public class Metadata implements Serializable { + + protected MSystem system; + + public Metadata(MetaCollector collector) throws MetadataException { + MetaObject[] sys = collector.getMetadata(MetadataType.SYSTEM, null); + if (sys != null && sys.length == 1) { + try { + this.system = (MSystem) sys[0]; + } catch (ClassCastException e) { + throw new MetadataException(e); + } + this.system.setCollector(collector); + } + } + + public Metadata(MSystem system) { + this.system = system; + } + + public MSystem getSystem() { + return this.system; + } + + public MResource getResource(String resourceId) { + return this.system.getMResource(resourceId); + } + + public MForeignKey getForeignKey(String foreignKeyId) { + return this.system.getMForeignKey(foreignKeyId); + } + + public MClass getMClass(String resourceId, String className) { + MResource resource = getResource(resourceId); + if (resource == null) { + return null; + } + return resource.getMClass(className); + } + + public MTable getTable(String resourceId, String className, String systemName) { + MClass clazz = getMClass(resourceId, className); + if (clazz == null) { + return null; + } + return clazz.getMTable(systemName); + } + + public MUpdate getUpdate(String resourceId, String className, String updateName) { + MClass clazz = getMClass(resourceId, className); + if (clazz == null) { + return null; + } + return clazz.getMUpdate(updateName); + } + + public MUpdateType getUpdateType(String resourceId, String className, String updateName, String systemName) { + MUpdate update = getUpdate(resourceId, className, updateName); + if (update == null) { + return null; + } + return update.getMUpdateType(systemName); + } + + public MObject getObject(String resourceId, String objectType) { + MResource resource = getResource(resourceId); + if (resource == null) { + return null; + } + return resource.getMObject(objectType); + } + + public MLookup getLookup(String resourceId, String lookupName) { + MResource resource = getResource(resourceId); + if (resource == null) { + return null; + } + return resource.getMLookup(lookupName); + } + + public MSearchHelp getSearchHelp(String resourceId, String searchHelpId) { + MResource resource = getResource(resourceId); + if (resource == null) { + return null; + } + return resource.getMSearchHelp(searchHelpId); + } + + public MValidationExternal getValidationExternal(String resourceId, String validationExternalName) { + MResource resource = getResource(resourceId); + if (resource == null) { + return null; + } + return resource.getMValidationExternal(validationExternalName); + } + + public MValidationLookup getValidationLookup(String resourceId, String validationLookupName) { + MResource resource = getResource(resourceId); + if (resource == null) { + return null; + } + return resource.getMValidationLookup(validationLookupName); + } + + private String getResourceId(MetaObject obj) { + String path = obj.getPath(); + int index = path.indexOf(':'); + if (index == -1) { + return null; + } + String resource = path.substring(0, index); + return resource; + } + + public MResource getResource(MTable field) { + String resource = getResourceId(field); + return getResource(resource); + } + + public MLookup getLookup(MTable field) { + String resource = getResourceId(field); + return getLookup(resource, field.getLookupName()); + } + + public MSearchHelp getSearchHelp(MTable field) { + String searchHelpID = field.getSearchHelpID(); + if (searchHelpID == null) { + return null; + } + String resource = getResourceId(field); + return getSearchHelp(resource, searchHelpID); + } + + public MResource getResource(MClass clazz) { + return getResource(getResourceId(clazz)); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataBuilder.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataBuilder.java new file mode 100644 index 0000000..19597cd --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataBuilder.java @@ -0,0 +1,203 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata; + +import com.ossez.usreio.tests.common.metadata.types.MClass; +import com.ossez.usreio.tests.common.metadata.types.MEditMask; +import com.ossez.usreio.tests.common.metadata.types.MForeignKey; +import com.ossez.usreio.tests.common.metadata.types.MLookup; +import com.ossez.usreio.tests.common.metadata.types.MLookupType; +import com.ossez.usreio.tests.common.metadata.types.MObject; +import com.ossez.usreio.tests.common.metadata.types.MResource; +import com.ossez.usreio.tests.common.metadata.types.MSearchHelp; +import com.ossez.usreio.tests.common.metadata.types.MSystem; +import com.ossez.usreio.tests.common.metadata.types.MTable; +import com.ossez.usreio.tests.common.metadata.types.MUpdate; +import com.ossez.usreio.tests.common.metadata.types.MUpdateHelp; +import com.ossez.usreio.tests.common.metadata.types.MUpdateType; +import com.ossez.usreio.tests.common.metadata.types.MValidationExpression; +import com.ossez.usreio.tests.common.metadata.types.MValidationExternal; +import com.ossez.usreio.tests.common.metadata.types.MValidationExternalType; +import com.ossez.usreio.tests.common.metadata.types.MValidationLookup; +import com.ossez.usreio.tests.common.metadata.types.MValidationLookupType; + +public abstract class MetadataBuilder { + protected MetadataBuilder() { + this.mStrict = false; + } + + public boolean isStrict() { + return this.mStrict; + } + + public void setStrict(boolean strict) { + this.mStrict = strict; + } + + protected Metadata finish(MSystem system) { + return new Metadata(system); + } + + protected static void setAttribute(MetaObject obj, String key, String value) { + obj.setAttribute(key, value); + } + + protected MSystem buildSystem() { + MSystem system = new MSystem(this.mStrict); + return system; + } + + protected MResource buildResource() { + MResource resource = new MResource(this.mStrict); + return resource; + } + + protected MForeignKey buildForeignKey() { + MForeignKey key = new MForeignKey(this.mStrict); + return key; + } + + protected MClass buildClass() { + MClass clazz = new MClass(this.mStrict); + return clazz; + } + + protected MTable buildTable() { + MTable table = new MTable(this.mStrict); + return table; + } + + protected MUpdate buildUpdate() { + MUpdate update = new MUpdate(this.mStrict); + return update; + } + + protected MUpdateType buildUpdateType() { + MUpdateType updatetype = new MUpdateType(this.mStrict); + return updatetype; + } + + protected MObject buildObject() { + MObject obj = new MObject(this.mStrict); + return obj; + } + + protected MSearchHelp buildSearchHelp() { + MSearchHelp help = new MSearchHelp(this.mStrict); + return help; + } + + protected MEditMask buildEditMask() { + MEditMask mask = new MEditMask(this.mStrict); + return mask; + } + + protected MLookup buildLookup() { + MLookup lookup = new MLookup(this.mStrict); + return lookup; + } + + protected MLookupType buildLookupType() { + MLookupType type = new MLookupType(this.mStrict); + return type; + } + + protected MUpdateHelp buildUpdateHelp() { + MUpdateHelp help = new MUpdateHelp(this.mStrict); + return help; + } + + protected MValidationLookup buildValidationLookup() { + MValidationLookup lookup = new MValidationLookup(this.mStrict); + return lookup; + } + + protected MValidationExternalType buildValidationExternalType() { + MValidationExternalType type = new MValidationExternalType(this.mStrict); + return type; + } + + protected MValidationExpression buildValidationExpression() { + MValidationExpression expression = new MValidationExpression(this.mStrict); + return expression; + } + + protected MValidationExternal buildValidationExternal() { + MValidationExternal external = new MValidationExternal(this.mStrict); + return external; + } + + protected MValidationLookupType buildValidationLookupType() { + MValidationLookupType lookupType = new MValidationLookupType(this.mStrict); + return lookupType; + } + + public abstract Metadata doBuild(Object src) throws MetadataException; + + public abstract MetaObject[] parse(Object src) throws MetadataException; + + protected MetaObject newType(MetadataType type) { + if (type == MetadataType.SYSTEM) { + return buildSystem(); + } + if (type == MetadataType.RESOURCE) { + return buildResource(); + } + if (type == MetadataType.FOREIGNKEYS) { + return buildForeignKey(); + } + if (type == MetadataType.CLASS) { + return buildClass(); + } + if (type == MetadataType.TABLE) { + return buildTable(); + } + if (type == MetadataType.UPDATE) { + return buildUpdate(); + } + if (type == MetadataType.UPDATE_TYPE) { + return buildUpdateType(); + } + if (type == MetadataType.OBJECT) { + return buildObject(); + } + if (type == MetadataType.SEARCH_HELP) { + return buildSearchHelp(); + } + if (type == MetadataType.EDITMASK) { + return buildEditMask(); + } + if (type == MetadataType.UPDATE_HELP) { + return buildUpdateHelp(); + } + if (type == MetadataType.LOOKUP) { + return buildLookup(); + } + if (type == MetadataType.LOOKUP_TYPE) { + return buildLookupType(); + } + if (type == MetadataType.VALIDATION_LOOKUP) { + return buildValidationLookup(); + } + if (type == MetadataType.VALIDATION_LOOKUP_TYPE) { + return buildValidationLookupType(); + } + if (type == MetadataType.VALIDATION_EXTERNAL) { + return buildValidationExternal(); + } + if (type == MetadataType.VALIDATION_EXTERNAL_TYPE) { + return buildValidationExternalType(); + } + if (type == MetadataType.VALIDATION_EXPRESSION) { + return buildValidationExpression(); + } + throw new RuntimeException("No metadata type class found for " + type.name()); + } + + private boolean mStrict; +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataElement.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataElement.java new file mode 100644 index 0000000..3afebc7 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataElement.java @@ -0,0 +1,31 @@ +package com.ossez.usreio.tests.common.metadata; + +public enum MetadataElement { + SYSTEM("System"),// might need to provide enumeration for different versions 1.5 vs 1.7 + RESOURCE("Resource"), + FOREIGNKEY("ForeignKey"), + CLASS("Class"), + TABLE("Field"), + UPDATE("UpdateType"), + UPDATETYPE("UpdateField"), + OBJECT("Object"), + SEARCHHELP("SearchHelp"), + EDITMASK("EditMask"), + UPDATEHELP("UpdateHelp"), + LOOKUP("Lookup"), + LOOKUPTYPE("LookupType"), + VALIDATIONLOOKUP("ValidationLookup"), + VALIDATIONLOOKUPTYPE("ValidationLookupType"), + VALIDATIONEXPRESSION("ValidationExpression"), + VALIDATIONEXTERNAL("ValidationExternalType"), + VALIDATIONEXTERNALTYPE("ValidationExternal"); + + private final String elementName; + + MetadataElement(String elementName){ + this.elementName = elementName; + } + + public String elementName(){ return this.elementName;} + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataException.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataException.java new file mode 100644 index 0000000..0ebb29d --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataException.java @@ -0,0 +1,27 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata; + + +public class MetadataException extends Exception { + public MetadataException() { + super(); + } + + public MetadataException(String msg) { + super(msg); + } + + public MetadataException(Throwable cause) { + super(cause); + } + + public MetadataException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataType.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataType.java new file mode 100644 index 0000000..61059ec --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/MetadataType.java @@ -0,0 +1,30 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata; + +public enum MetadataType { + EDITMASK, + FOREIGNKEYS, + RESOURCE, + LOOKUP, + LOOKUP_TYPE, + OBJECT, + SEARCH_HELP, + SYSTEM, + TABLE, + UPDATE, + UPDATE_HELP, + UPDATE_TYPE, + VALIDATION_EXPRESSION, + VALIDATION_EXTERNAL, + VALIDATION_EXTERNAL_TYPE, + VALIDATION_LOOKUP, + VALIDATION_LOOKUP_TYPE, + CLASS; + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrAbstractText.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrAbstractText.java new file mode 100644 index 0000000..139fac5 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrAbstractText.java @@ -0,0 +1,49 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata.attrib; + +import com.ossez.usreio.tests.common.metadata.AttrType; +import com.ossez.usreio.tests.common.metadata.MetaParseException; + +public abstract class AttrAbstractText implements AttrType { + protected int min; + protected int max; + + public AttrAbstractText(int min, int max) { + this.min = min; + this.max = max; + } + + + public String parse(String value, boolean strict) throws MetaParseException { + if( !strict ) + return value; + int l = value.length(); + if (this.min != 0 && l < this.min) { + throw new MetaParseException("Value too short (min " + this.min + "): " + l); + } + if (this.max != 0 && l > this.max) { + throw new MetaParseException("Value too long (max " + this.max + "): " + l); + } + checkContent(value); + return value; + } + + + public Class getType() { + return String.class; + } + + + public String render(String value) { + return value; + } + + protected abstract void checkContent(String value) throws MetaParseException; + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrAlphanum.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrAlphanum.java new file mode 100644 index 0000000..54d07a2 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrAlphanum.java @@ -0,0 +1,31 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata.attrib; + +import com.ossez.usreio.tests.common.metadata.MetaParseException; + +public class AttrAlphanum extends AttrAbstractText { + + public AttrAlphanum(int min, int max) { + super(min, max); + } + + @Override + protected void checkContent(String value) throws MetaParseException { + char[] chars = value.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (!Character.isLetterOrDigit(c)) { + // illegal but exist in CRT metadata + if ("_- ".indexOf(c) == -1) { + throw new MetaParseException("Invalid Alphanum character at position " + i + ": " + c); + } + } + } + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrBoolean.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrBoolean.java new file mode 100644 index 0000000..ac3a3b8 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrBoolean.java @@ -0,0 +1,54 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata.attrib; + +import com.ossez.usreio.tests.common.metadata.AttrType; +import com.ossez.usreio.tests.common.metadata.MetaParseException; + +public class AttrBoolean implements AttrType { + public Boolean parse(String value, boolean strict) throws MetaParseException { + if (value.equals("1")) { + return Boolean.TRUE; + } + if (value.equals("0")) { + return Boolean.FALSE; + } + + if (value.equalsIgnoreCase("true")) { + return Boolean.TRUE; + } + if (value.equalsIgnoreCase("false")) { + return Boolean.FALSE; + } + + if (value.equalsIgnoreCase("Y")) { + return Boolean.TRUE; + } + if (value.equalsIgnoreCase("N")) { + return Boolean.FALSE; + } + + if (value.equals("")) { + return Boolean.FALSE; + } + + if( strict ) + throw new MetaParseException("Invalid boolean value: " + value); + return false; + } + + public String render(Boolean value) { + if( value.booleanValue() ) return "1"; + + return "0"; + } + + public Class getType() { + return Boolean.class; + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrDate.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrDate.java new file mode 100644 index 0000000..425802f --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrDate.java @@ -0,0 +1,71 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + * + * + * Vangulo Changed: + * gives ability to handle dates in this format + * 2011-06-01T18:06:58 + * should find a more elegant way + */ +package com.ossez.usreio.tests.common.metadata.attrib; + +//import java.text.DateFormat; +//import java.text.ParseException; +//import java.text.SimpleDateFormat; +//import java.util.Date; + +import com.ossez.usreio.tests.common.metadata.AttrType; +import com.ossez.usreio.tests.common.metadata.MetaParseException; + +/** + * Converted this class to return a String instead of a + * Date object which allows for more flexiblity since + * Many Rets Servers format their dates differently + * + * @author vangulo + * + */ +public class AttrDate implements AttrType { + + // need date attribute to be flexible since different MLS's have + // different formats for dates + public String parse(String value, boolean strict) throws MetaParseException { + return value; +// Date d; +// try { +// d = this.df.parse(value); +// } catch (ParseException e) { +// if( strict ) +// throw new MetaParseException(e); +// try { +// value = value.replaceAll("[A-Za-z]", " "); +// d = this.df1.parse(value); +// } catch (ParseException e1) { +// //e1.printStackTrace(); +// return value; +// } +// return d; +// } +// return d; + } + + public String render(String value) { + return value; + //Date date = value; + //return this.df.format(date); + } + + public Class getType() { + return String.class; + } + + //private DateFormat df = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss z"); + //2011-06-01T18:06:58 + //private DateFormat df1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + //Tuesday, 22-Dec-2009 21:03:18 GMT + //private DateFormat df2 = new SimpleDateFormat("E, dd-MMM-yyyy HH:mm:ss z"); +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrEnum.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrEnum.java new file mode 100644 index 0000000..2464d57 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrEnum.java @@ -0,0 +1,31 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata.attrib; + +import java.util.HashMap; +import java.util.Map; +import java.util.Collections; + +import com.ossez.usreio.tests.common.metadata.MetaParseException; + +public class AttrEnum extends AttrAbstractText { + public AttrEnum(String[] values) { + super(0, 0); + this.map = new HashMap(); + for (String value : values) this.map.put(value, value); + this.map = Collections.unmodifiableMap(this.map); + } + + @Override + protected void checkContent(String value) throws MetaParseException { + if( !this.map.containsKey(value) ) + throw new MetaParseException("Invalid key: " + value); + } + + private Map map; +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrGenericText.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrGenericText.java new file mode 100644 index 0000000..4c411f2 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrGenericText.java @@ -0,0 +1,31 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata.attrib; + +import com.ossez.usreio.tests.common.metadata.MetaParseException; + +public class AttrGenericText extends AttrAbstractText { + private String mChars; + + public AttrGenericText(int min, int max, String chars) { + super(min, max); + this.mChars = chars; + } + + @Override + protected void checkContent(String value) throws MetaParseException { + char[] chars = value.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (this.mChars.indexOf(c) == -1) { + throw new MetaParseException("Invalid char (" + c + ") at position " + i); + } + } + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrNumeric.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrNumeric.java new file mode 100644 index 0000000..91d8e48 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrNumeric.java @@ -0,0 +1,31 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata.attrib; + +import com.ossez.usreio.tests.common.metadata.AttrType; +import com.ossez.usreio.tests.common.metadata.MetaParseException; + +public class AttrNumeric implements AttrType { + public Integer parse(String value, boolean strict) throws MetaParseException { + try { + return new Integer(value); + } catch (NumberFormatException e) { + if( strict ) + throw new MetaParseException(e); + return 0; + } + } + + public String render(Integer value) { + return value.toString(); + } + + public Class getType() { + return Integer.class; + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrNumericPositive.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrNumericPositive.java new file mode 100644 index 0000000..c3126ba --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrNumericPositive.java @@ -0,0 +1,36 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata.attrib; + +import com.ossez.usreio.tests.common.metadata.AttrType; +import com.ossez.usreio.tests.common.metadata.MetaParseException; + +public class AttrNumericPositive implements AttrType { + + public Integer parse(String value, boolean strict) throws MetaParseException { + try { + Integer integer = new Integer(value); + if (strict && integer < 1) throw new IllegalArgumentException(String.format("%s is not positive", value)); + return integer; + } catch (Exception e) { + if( strict ) + throw new MetaParseException(e); + return 1; + } + } + + + public String render(Integer value) { + return value.toString(); + } + + + public Class getType() { + return Integer.class; + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrPlaintext.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrPlaintext.java new file mode 100644 index 0000000..2a707dc --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrPlaintext.java @@ -0,0 +1,28 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata.attrib; + +import com.ossez.usreio.tests.common.metadata.MetaParseException; + +public class AttrPlaintext extends AttrAbstractText { + public AttrPlaintext(int min, int max) { + super(min, max); + } + + @Override + protected void checkContent(String value) throws MetaParseException { + char[] chars = value.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (c < 31 || c > 126) { + throw new MetaParseException("Invalid character (ordinal " + (int) c + ") at position " + i); + } + } + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrText.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrText.java new file mode 100644 index 0000000..20772a4 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrText.java @@ -0,0 +1,28 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata.attrib; + +import com.ossez.usreio.tests.common.metadata.MetaParseException; + +public class AttrText extends AttrAbstractText { + public AttrText(int min, int max) { + super(min, max); + } + + @Override + protected void checkContent(String value) throws MetaParseException { + char[] chars = value.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (!(c == '\n' || c == '\r' || c == ' ' || c == '\t' || (c > 31 && c < 127))) { + throw new MetaParseException("Invalid character (ordinal " + (int) c + ") at position " + i); + } + } + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrVersion.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrVersion.java new file mode 100644 index 0000000..2ae194b --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/attrib/AttrVersion.java @@ -0,0 +1,66 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.common.metadata.attrib; + +import org.apache.commons.lang3.StringUtils; +import com.ossez.usreio.tests.common.metadata.AttrType; +import com.ossez.usreio.tests.common.metadata.MetaParseException; + +/** + * A version is a string formatted "major.minor.release". This gets converted + * to an integer such as major * 10,000,000 + minor * 100,000 + release. + */ +public class AttrVersion implements AttrType { + + public Integer parse(String value, boolean strict) throws MetaParseException { + String[] parts = StringUtils.split(value, "."); + int major, minor, release; + if (strict && parts != null && parts.length != 3) { + throw new MetaParseException("Invalid version: " + value + ", " + parts.length + " parts"); + } + try { + major = Integer.parseInt(this.getPart(parts,0)); + minor = Integer.parseInt(this.getPart(parts,1)); + release = Integer.parseInt(this.getPart(parts,2)); + } catch (NumberFormatException e) { + throw new MetaParseException("Invalid version: " + value, e); + } + if ((major < 100) && (major >= 0) && (minor < 100) && (minor >= 0) && (release < 100000) && (release >= 0)) { + return new Integer(major * 10000000 + minor * 100000 + release); + } + if( strict ) + throw new MetaParseException("Invalid version: " + value); + return 0; + } + private String getPart(String[] parts, int part){ + if( parts != null && parts.length > part ) return parts[part]; + return "0"; + } + + + public String render(Integer value) { + int ver = value.intValue(); + int release = ver % 100000; + int minor = (ver / 100000) % 100; + int major = (ver / 10000000); + String minstr = Integer.toString(minor); + String relstr = Integer.toString(release); + while (minstr.length() < 2) { + minstr = "0" + minstr; + } + while (relstr.length() < 5) { + relstr = "0" + relstr; + } + return major + "." + minstr + "." + relstr; + } + + + public Class getType() { + return Integer.class; + } +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MClass.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MClass.java new file mode 100644 index 0000000..665185a --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MClass.java @@ -0,0 +1,126 @@ +package com.ossez.usreio.tests.common.metadata.types; + +import java.util.Collection; +//import java.util.Date; +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MClass extends MetaObject { + public static final String CLASSNAME = "ClassName"; + public static final String VISIBLENAME = "VisibleName"; + public static final String STANDARDNAME = "StandardName"; + public static final String DESCRIPTION = "Description"; + public static final String TABLEVERSION = "TableVersion"; + public static final String TABLEDATE = "TableDate"; + public static final String UPDATEVERSION = "UpdateVersion"; + public static final String UPDATEDATE = "UpdateDate"; + public static final String DELETEDFLAGFIELD = "DeletedFlagField"; + public static final String DELETEDFLAGVALUE = "DeletedFlagValue"; + public static final String CLASSTIMESTAMP = "ClassTimeStamp"; + public static final String HASHKEYINDEX = "HasKeyIndex"; + private static MetadataType[] sTypes = { MetadataType.UPDATE, MetadataType.TABLE }; + + public MClass() { + this(DEFAULT_PARSING); + } + + public MClass(boolean strictParsing) { + super(strictParsing); + } + + @Override + public MetadataType[] getChildTypes() { + return sTypes; + } + + public String getClassName() { + return getStringAttribute(CLASSNAME); + } + + public String getVisibleName() { + return getStringAttribute(VISIBLENAME); + } + + public String getStandardName() { + return getStringAttribute(STANDARDNAME); + } + + public String getDescription() { + return getStringAttribute(DESCRIPTION); + } + + public int getTableVersion() { + return getIntAttribute(TABLEVERSION); + } + + public String getTableDate() { + return getDateAttribute(TABLEDATE); + } + + public int getUpdateVersion() { + return getIntAttribute(UPDATEVERSION); + } + + public String getUpdateDate() { + return getDateAttribute(UPDATEDATE); + } + + public MUpdate getMUpdate(String updateName) { + return (MUpdate) getChild(MetadataType.UPDATE, updateName); + } + + public MUpdate[] getMUpdates() { + MUpdate[] tmpl = new MUpdate[0]; + return (MUpdate[]) getChildren(MetadataType.UPDATE).toArray(tmpl); + } + + public MTable getMTable(String systemName) { + return (MTable) getChild(MetadataType.TABLE, systemName); + } + + public MTable[] getMTables() { + Collection children = getChildren(MetadataType.TABLE); + return (MTable[]) children.toArray(new MTable[0]); + } + + @Override + protected String getIdAttr() { + return CLASSNAME; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(CLASSNAME, sAlphanum32); + attributeMap.put(VISIBLENAME, sPlaintext32); + attributeMap.put(STANDARDNAME, sAlphanum32); + attributeMap.put(DESCRIPTION, sPlaintext128); + attributeMap.put(TABLEVERSION, sAttrVersion); + attributeMap.put(TABLEDATE, sAttrDate); + attributeMap.put(UPDATEVERSION, sAttrVersion); + attributeMap.put(UPDATEDATE, sAttrDate); + attributeMap.put(DELETEDFLAGFIELD, retsname); + attributeMap.put(DELETEDFLAGVALUE, sAlphanum32); + attributeMap.put(CLASSTIMESTAMP, retsname); + attributeMap.put(HASHKEYINDEX, sAttrBoolean); + } + + + public String getDeletedFlagField() { + return getStringAttribute(DELETEDFLAGFIELD); + } + + public String getDeletedFlagValue() { + return getStringAttribute(DELETEDFLAGVALUE); + } + + public String getClassTimeStamp() { + return getStringAttribute(CLASSTIMESTAMP); + } + + public String getHashKeyIndex() { + return getStringAttribute(HASHKEYINDEX); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MEditMask.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MEditMask.java new file mode 100644 index 0000000..7ccca7e --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MEditMask.java @@ -0,0 +1,50 @@ +package com.ossez.usreio.tests.common.metadata.types; + +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MEditMask extends MetaObject { + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String EDITMASKID = "EditMaskID"; + public static final String VALUE = "Value"; + + public MEditMask() { + this(DEFAULT_PARSING); + } + + public MEditMask(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getEditMaskID() { + return getStringAttribute(EDITMASKID); + } + + public String getValue() { + return getStringAttribute(VALUE); + } + + @Override + public MetadataType[] getChildTypes() { + return sNoChildren; + } + + @Override + protected String getIdAttr() { + return EDITMASKID; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAttrMetadataEntryId); + attributeMap.put(EDITMASKID, sAlphanum32); + attributeMap.put(VALUE, sText256); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MForeignKey.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MForeignKey.java new file mode 100644 index 0000000..7ae1de7 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MForeignKey.java @@ -0,0 +1,87 @@ +package com.ossez.usreio.tests.common.metadata.types; + +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MForeignKey extends MetaObject { + public static final String FOREIGNKEYID = "ForeignKeyID"; + public static final String PARENTRESOURCEID = "ParentResourceID"; + public static final String PARENTCLASSID = "ParentClassID"; + public static final String PARENTSYSTEMNAME = "ParentSystemName"; + public static final String CHILDRESOURCEID = "ChildResourceID"; + public static final String CHILDCLASSID = "ChildClassID"; + public static final String CHILDSYSTEMNAME = "ChildSystemName"; + public static final String CONDITIONALPARENTFIELD = "ConditionalParentField"; + public static final String CONDITIONALPARENTVALUE = "ConditionalParentValue"; + + + public MForeignKey() { + this(DEFAULT_PARSING); + } + + public MForeignKey(boolean strictParsing) { + super(strictParsing); + } + + public String getForeignKeyID() { + return getStringAttribute(FOREIGNKEYID); + } + + public String getParentResourceID() { + return getStringAttribute(PARENTRESOURCEID); + } + + public String getParentClassID() { + return getStringAttribute(PARENTCLASSID); + } + + public String getParentSystemName() { + return getStringAttribute(PARENTSYSTEMNAME); + } + + public String getChildResourceID() { + return getStringAttribute(CHILDRESOURCEID); + } + + public String getChildClassID() { + return getStringAttribute(CHILDCLASSID); + } + + public String getChildSystemName() { + return getStringAttribute(CHILDSYSTEMNAME); + } + + public String getConditionalParentField() { + return getStringAttribute(CONDITIONALPARENTFIELD); + } + + public String getConditionalParentValue() { + return getStringAttribute(CONDITIONALPARENTVALUE); + } + + @Override + public MetadataType[] getChildTypes() { + return sNoChildren; + } + + @Override + protected String getIdAttr() { + return FOREIGNKEYID; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(FOREIGNKEYID, sAlphanum32); + attributeMap.put(PARENTRESOURCEID, sAlphanum32); + attributeMap.put(PARENTCLASSID, sAlphanum32); + attributeMap.put(PARENTSYSTEMNAME, sAlphanum32); + attributeMap.put(CHILDRESOURCEID, sAlphanum32); + attributeMap.put(CHILDCLASSID, sAlphanum32); + attributeMap.put(CHILDSYSTEMNAME, sAlphanum32); + attributeMap.put(CONDITIONALPARENTFIELD, retsname); + attributeMap.put(CONDITIONALPARENTVALUE, retsname); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MLookup.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MLookup.java new file mode 100644 index 0000000..144c5ed --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MLookup.java @@ -0,0 +1,86 @@ +package com.ossez.usreio.tests.common.metadata.types; + +//import java.util.Date; +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MLookup extends MetaObject { + private static final MetadataType[] CHILDREN = { MetadataType.LOOKUP_TYPE }; + private static final MLookupType[] EMPTYLOOKUPTYPES = {}; + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String LOOKUPNAME = "LookupName"; + public static final String VISIBLENAME = "VisibleName"; + public static final String VERSION = "Version"; + public static final String DATE = "Date"; + public static final String LOOKUPTYPEVERSION = "LookupTypeVersion"; + public static final String LOOKUPTYPEDATE = "LookupTypeDate"; + + public MLookup() { + this(DEFAULT_PARSING); + } + + public MLookup(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getLookupName() { + return getStringAttribute(LOOKUPNAME); + } + + public String getVisibleName() { + return getStringAttribute(VISIBLENAME); + } + + public int getVersion() { + + int ver = getIntAttribute(VERSION); + if (ver == 0){ + ver = getIntAttribute(LOOKUPTYPEVERSION); + } + return ver; + } + + public String getDate() { + String date = getDateAttribute(DATE); + if (date == null) { + date = getDateAttribute(LOOKUPTYPEDATE); + } + return date; + } + + public MLookupType getMLookupType(String value) { + return (MLookupType) getChild(MetadataType.LOOKUP_TYPE, value); + } + + public MLookupType[] getMLookupTypes() { + return (MLookupType[]) getChildren(MetadataType.LOOKUP_TYPE).toArray(EMPTYLOOKUPTYPES); + } + + @Override + public MetadataType[] getChildTypes() { + return CHILDREN; + } + + @Override + protected String getIdAttr() { + return LOOKUPNAME; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAttrMetadataEntryId); + attributeMap.put(LOOKUPNAME, sAlphanum32); + attributeMap.put(VISIBLENAME, sPlaintext32); + attributeMap.put(VERSION, sAttrVersion); + attributeMap.put(DATE, sAttrDate); + attributeMap.put(LOOKUPTYPEVERSION, sAttrVersion); + attributeMap.put(LOOKUPTYPEDATE, sAttrDate); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MLookupType.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MLookupType.java new file mode 100644 index 0000000..f1917f7 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MLookupType.java @@ -0,0 +1,56 @@ +package com.ossez.usreio.tests.common.metadata.types; + +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MLookupType extends MetaObject { + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String LONGVALUE = "LongValue"; + public static final String SHORTVALUE = "ShortValue"; + public static final String VALUE = "Value"; + + public MLookupType() { + this(DEFAULT_PARSING); + } + + public MLookupType(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getLongValue() { + return getStringAttribute(LONGVALUE); + } + + public String getShortValue() { + return getStringAttribute(SHORTVALUE); + } + + public String getValue() { + return getStringAttribute(VALUE); + } + + @Override + public MetadataType[] getChildTypes() { + return sNoChildren; + } + + @Override + protected String getIdAttr() { + return VALUE; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAttrMetadataEntryId); + attributeMap.put(LONGVALUE, sText128); + attributeMap.put(SHORTVALUE, sText32); + attributeMap.put(VALUE, sAlphanum32); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MObject.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MObject.java new file mode 100644 index 0000000..53a9e47 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MObject.java @@ -0,0 +1,79 @@ +package com.ossez.usreio.tests.common.metadata.types; + +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MObject extends MetaObject { + + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String OBJECTTYPE = "ObjectType"; + public static final String MIMETYPE = "MimeType"; + public static final String VISIBLENAME = "VisibleName"; + public static final String DESCRIPTION = "Description"; + public static final String OBJECTTIMESTAMPNAME = "ObjectTimeStamp"; + public static final String OBJECTCOUNT = "ObjectCount"; + public static final String STANDARDNAME = "StandardName"; + public MObject() { + this(DEFAULT_PARSING); + } + + public MObject(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getObjectType() { + return getStringAttribute(OBJECTTYPE); + } + + public String getMIMEType() { + return getStringAttribute(MIMETYPE); + } + + public String getVisibleName() { + return getStringAttribute(VISIBLENAME); + } + + public String getDescription() { + return getStringAttribute(DESCRIPTION); + } + public String getStandardName() { + return getStringAttribute(STANDARDNAME); + } + + @Override + public MetadataType[] getChildTypes() { + return sNoChildren; + } + + @Override + protected String getIdAttr() { + return OBJECTTYPE; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAlphanum24); + attributeMap.put(OBJECTTYPE, sAlphanum24); + attributeMap.put(MIMETYPE, sText64); + attributeMap.put(VISIBLENAME, sPlaintext64); + attributeMap.put(DESCRIPTION, sPlaintext128); + attributeMap.put(OBJECTTIMESTAMPNAME, retsname); + attributeMap.put(OBJECTCOUNT, retsname); + attributeMap.put(STANDARDNAME, retsname); + } + + public String getObjectTimeStampName() { + return getStringAttribute(OBJECTTIMESTAMPNAME); + } + + public String getObjectCount() { + return getStringAttribute(OBJECTCOUNT); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MResource.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MResource.java new file mode 100644 index 0000000..a59a13e --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MResource.java @@ -0,0 +1,270 @@ +package com.ossez.usreio.tests.common.metadata.types; + +//import java.util.Date; +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MResource extends MetaObject { + private static final MetadataType[] CHILDREN = { + MetadataType.VALIDATION_EXPRESSION, + MetadataType.LOOKUP, + MetadataType.CLASS, + MetadataType.OBJECT, + MetadataType.VALIDATION_EXTERNAL, + MetadataType.VALIDATION_LOOKUP, + MetadataType.EDITMASK, + MetadataType.UPDATE_HELP, + MetadataType.SEARCH_HELP + }; + + public static final String RESOURCEID = "ResourceID"; + public static final String STANDARDNAME = "StandardName"; + public static final String VISIBLENAME = "VisibleName"; + public static final String DESCRIPTION = "Description"; + public static final String KEYFIELD = "KeyField"; + public static final String CLASSCOUNT = "ClassCount"; + public static final String CLASSVERSION = "ClassVersion"; + public static final String CLASSDATE = "ClassDate"; + public static final String OBJECTVERSION = "ObjectVersion"; + public static final String OBJECTDATE = "ObjectDate"; + public static final String SEARCHHELPVERSION = "SearchHelpVersion"; + public static final String SEARCHHELPDATE = "SearchHelpDate"; + public static final String EDITMASKVERSION = "EditMaskVersion"; + public static final String EDITMASKDATE = "EditMaskDate"; + public static final String LOOKUPVERSION = "LookupVersion"; + public static final String LOOKUPDATE = "LookupDate"; + public static final String UPDATEHELPVERSION = "UpdateHelpVersion"; + public static final String UPDATEHELPDATE = "UpdateHelpDate"; + public static final String VALIDATIONEXPRESSIONVERSION = "ValidationExpressionVersion"; + public static final String VALIDATIONEXPRESSIONDATE = "ValidationExpressionDate"; + public static final String VALIDATIONLOOKUPVERSION = "ValidationLookupVersion"; + public static final String VALIDATIONLOOKUPDATE = "ValidationLookupDate"; + public static final String VALIDATIONEXTERNALVERSION = "ValidationExternalVersion"; + public static final String VALIDATIONEXTERNALDATE = "ValidationExternalDate"; + + public MResource() { + this(DEFAULT_PARSING); + } + + public MResource(boolean strictParsing) { + super(strictParsing); + } + + public String getResourceID() { + return getStringAttribute(RESOURCEID); + } + + public String getStandardName() { + return getStringAttribute(STANDARDNAME); + } + + public String getVisibleName() { + return getStringAttribute(VISIBLENAME); + } + + public String getDescription() { + return getStringAttribute(DESCRIPTION); + } + + public String getKeyField() { + return getStringAttribute(KEYFIELD); + } + + public int getClassCount() { + return getIntAttribute(CLASSCOUNT); + } + + public int getClassVersion() { + return getIntAttribute(CLASSVERSION); + } + + public String getClassDate() { + return getDateAttribute(CLASSDATE); + } + + public int getObjectVersion() { + return getIntAttribute(OBJECTVERSION); + } + + public String getObjectDate() { + return getDateAttribute(OBJECTDATE); + } + + public int getSearchHelpVersion() { + return getIntAttribute(SEARCHHELPVERSION); + } + + public String getSearchHelpDate() { + return getDateAttribute(SEARCHHELPDATE); + } + + public int getEditMaskVersion() { + return getIntAttribute(EDITMASKVERSION); + } + + public String getEditMaskDate() { + return getDateAttribute(EDITMASKDATE); + } + + public int getLookupVersion() { + return getIntAttribute(LOOKUPVERSION); + } + + public String getLookupDate() { + return getDateAttribute(LOOKUPDATE); + } + + public int getUpdateHelpVersion() { + return getIntAttribute(UPDATEHELPVERSION); + } + + public String getUpdateHelpDate() { + return getDateAttribute(UPDATEHELPDATE); + } + + public int getValidationExpressionVersion() { + return getIntAttribute(VALIDATIONEXPRESSIONVERSION); + } + + public String getValidationExpressionDate() { + return getDateAttribute(VALIDATIONEXPRESSIONDATE); + } + + public int getValidationLookupVersion() { + return getIntAttribute(VALIDATIONLOOKUPVERSION); + } + + public String getValidationLookupDate() { + return getDateAttribute(VALIDATIONLOOKUPDATE); + } + + public int getValidationExternalVersion() { + return getIntAttribute(VALIDATIONEXTERNALVERSION); + } + + public String getValidationExternalDate() { + return getDateAttribute(VALIDATIONEXTERNALDATE); + } + + public MValidationExpression getMValidationExpression(String validationExpressionID) { + return (MValidationExpression) getChild(MetadataType.VALIDATION_EXPRESSION, validationExpressionID); + } + + public MValidationExpression[] getMValidationExpressions() { + MValidationExpression[] tmpl = new MValidationExpression[0]; + return (MValidationExpression[]) getChildren(MetadataType.VALIDATION_EXPRESSION).toArray(tmpl); + } + + public MLookup getMLookup(String lookupName) { + return (MLookup) getChild(MetadataType.LOOKUP, lookupName); + } + + public MLookup[] getMLookups() { + MLookup[] tmpl = new MLookup[0]; + return (MLookup[]) getChildren(MetadataType.LOOKUP).toArray(tmpl); + } + + public MClass getMClass(String className) { + return (MClass) getChild(MetadataType.CLASS, className); + } + + public MClass[] getMClasses() { + MClass[] tmpl = new MClass[0]; + return (MClass[]) getChildren(MetadataType.CLASS).toArray(tmpl); + } + + public MObject getMObject(String objectType) { + return (MObject) getChild(MetadataType.OBJECT, objectType); + } + + public MObject[] getMObjects() { + MObject[] tmpl = new MObject[0]; + return (MObject[]) getChildren(MetadataType.OBJECT).toArray(tmpl); + } + + public MValidationExternal getMValidationExternal(String validationExternalName) { + return (MValidationExternal) getChild(MetadataType.VALIDATION_EXTERNAL, validationExternalName); + } + + public MValidationExternal[] getMValidationExternal() { + MValidationExternal[] tmpl = new MValidationExternal[0]; + return (MValidationExternal[]) getChildren(MetadataType.VALIDATION_EXTERNAL).toArray(tmpl); + } + + public MValidationLookup getMValidationLookup(String validationLookupName) { + return (MValidationLookup) getChild(MetadataType.VALIDATION_LOOKUP, validationLookupName); + } + + public MValidationLookup[] getMValidationLookups() { + MValidationLookup[] tmpl = new MValidationLookup[0]; + return (MValidationLookup[]) getChildren(MetadataType.VALIDATION_LOOKUP).toArray(tmpl); + } + + public MEditMask getMEditMask(String editMaskID) { + return (MEditMask) getChild(MetadataType.EDITMASK, editMaskID); + } + + public MEditMask[] getMEditMasks() { + MEditMask[] tmpl = new MEditMask[0]; + return (MEditMask[]) getChildren(MetadataType.EDITMASK).toArray(tmpl); + } + + public MUpdateHelp getMUpdateHelp(String updateHelpID) { + return (MUpdateHelp) getChild(MetadataType.UPDATE_HELP, updateHelpID); + } + + public MUpdateHelp[] getMUpdateHelps() { + MUpdateHelp[] tmpl = new MUpdateHelp[0]; + return (MUpdateHelp[]) getChildren(MetadataType.UPDATE_HELP).toArray(tmpl); + } + + public MSearchHelp getMSearchHelp(String searchHelpID) { + return (MSearchHelp) getChild(MetadataType.SEARCH_HELP, searchHelpID); + } + + public MSearchHelp[] getMSearchHelps() { + MSearchHelp[] tmpl = new MSearchHelp[0]; + return (MSearchHelp[]) getChildren(MetadataType.SEARCH_HELP).toArray(tmpl); + } + + @Override + public MetadataType[] getChildTypes() { + return CHILDREN; + } + + @Override + protected String getIdAttr() { + return RESOURCEID; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(RESOURCEID, sAlphanum32); + attributeMap.put(STANDARDNAME, sAlphanum32); + attributeMap.put(VISIBLENAME, sPlaintext32); + attributeMap.put(DESCRIPTION, sPlaintext64); + attributeMap.put(KEYFIELD, sAlphanum32); + attributeMap.put(CLASSCOUNT, sAttrNumeric); + attributeMap.put(CLASSVERSION, sAttrVersion); + attributeMap.put(CLASSDATE, sAttrDate); + attributeMap.put(OBJECTVERSION, sAttrVersion); + attributeMap.put(OBJECTDATE, sAttrDate); + attributeMap.put(SEARCHHELPVERSION, sAttrVersion); + attributeMap.put(SEARCHHELPDATE, sAttrDate); + attributeMap.put(EDITMASKVERSION, sAttrVersion); + attributeMap.put(EDITMASKDATE, sAttrDate); + attributeMap.put(LOOKUPVERSION, sAttrVersion); + attributeMap.put(LOOKUPDATE, sAttrDate); + attributeMap.put(UPDATEHELPVERSION, sAttrVersion); + attributeMap.put(UPDATEHELPDATE, sAttrDate); + attributeMap.put(VALIDATIONEXPRESSIONVERSION, sAttrVersion); + attributeMap.put(VALIDATIONEXPRESSIONDATE, sAttrDate); + attributeMap.put(VALIDATIONLOOKUPVERSION, sAttrVersion); + attributeMap.put(VALIDATIONLOOKUPDATE, sAttrDate); + attributeMap.put(VALIDATIONEXTERNALVERSION, sAttrVersion); + attributeMap.put(VALIDATIONEXTERNALDATE, sAttrDate); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MSearchHelp.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MSearchHelp.java new file mode 100644 index 0000000..3b04d32 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MSearchHelp.java @@ -0,0 +1,50 @@ +package com.ossez.usreio.tests.common.metadata.types; + +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MSearchHelp extends MetaObject { + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String SEARCHHELPID = "SearchHelpID"; + public static final String VALUE = "Value"; + + public MSearchHelp() { + this(DEFAULT_PARSING); + } + + public MSearchHelp(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getSearchHelpID() { + return getStringAttribute(SEARCHHELPID); + } + + public String getValue() { + return getStringAttribute(VALUE); + } + + @Override + public MetadataType[] getChildTypes() { + return sNoChildren; + } + + @Override + protected String getIdAttr() { + return SEARCHHELPID; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAttrMetadataEntryId); + attributeMap.put(SEARCHHELPID, sAlphanum32); + attributeMap.put(VALUE, sText1024); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MSystem.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MSystem.java new file mode 100644 index 0000000..2ebf5bd --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MSystem.java @@ -0,0 +1,89 @@ +package com.ossez.usreio.tests.common.metadata.types; + +//import java.util.Date; +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MSystem extends MetaObject { + public static final String SYSTEMID = "SystemID"; + public static final String SYSTEMDESCRIPTION = "SystemDescription"; + public static final String COMMENTS = "Comments"; + public static final String DATE = "Date"; + public static final String VERSION = "Version"; + public static final String TIMEZONEOFFSET = "TimeZoneOffset"; + + public MSystem() { + this(DEFAULT_PARSING); + } + + public MSystem(boolean strictParsing) { + super(strictParsing); + } + + public String getSystemID() { + return getStringAttribute(SYSTEMID); + } + + public String getComment() { + return getStringAttribute(COMMENTS); + } + + public String getSystemDescription() { + return getStringAttribute(SYSTEMDESCRIPTION); + } + + public String getDate() { + return getDateAttribute(DATE); + } + + public String getTimeZoneOffset() { + return getDateAttribute(TIMEZONEOFFSET); + } + + public int getVersion() { + return getIntAttribute(VERSION); + } + + public MResource getMResource(String resourceID) { + return (MResource) getChild(MetadataType.RESOURCE, resourceID); + } + + public MResource[] getMResources() { + MResource[] tmpl = new MResource[0]; + return (MResource[]) getChildren(MetadataType.RESOURCE).toArray(tmpl); + } + + public MForeignKey getMForeignKey(String foreignKeyID) { + return (MForeignKey) getChild(MetadataType.FOREIGNKEYS, foreignKeyID); + } + + public MForeignKey[] getMForeignKeys() { + MForeignKey[] tmpl = new MForeignKey[0]; + return (MForeignKey[]) getChildren(MetadataType.FOREIGNKEYS).toArray(tmpl); + } + + @Override + public MetadataType[] getChildTypes() { + return CHILDREN; + } + + @Override + protected String getIdAttr() { + return null; + } + + public static final MetadataType[] CHILDREN = { MetadataType.RESOURCE, MetadataType.FOREIGNKEYS }; + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(SYSTEMID, sAlphanum10); + attributeMap.put(SYSTEMDESCRIPTION, sPlaintext64); + attributeMap.put(DATE, sAttrDate); + attributeMap.put(VERSION, sAttrVersion); + attributeMap.put(COMMENTS, sText); + attributeMap.put(TIMEZONEOFFSET, sAttrDate); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MTable.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MTable.java new file mode 100644 index 0000000..9b6f8ce --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MTable.java @@ -0,0 +1,242 @@ +package com.ossez.usreio.tests.common.metadata.types; + +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.AttrType; +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; +import com.ossez.usreio.tests.common.metadata.attrib.AttrEnum; + +public class MTable extends MetaObject { + + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String SYSTEMNAME = "SystemName"; + public static final String STANDARDNAME = "StandardName"; + public static final String LONGNAME = "LongName"; + public static final String DBNAME = "DBName"; + public static final String SHORTNAME = "ShortName"; + public static final String MAXIMUMLENGTH = "MaximumLength"; + public static final String DATATYPE = "DataType"; + public static final String PRECISION = "Precision"; + public static final String SEARCHABLE = "Searchable"; + public static final String INTERPRETATION = "Interpretation"; + public static final String ALIGNMENT = "Alignment"; + public static final String USESEPARATOR = "UseSeparator"; + public static final String EDITMASKID = "EditMaskID"; + public static final String LOOKUPNAME = "LookupName"; + public static final String MAXSELECT = "MaxSelect"; + public static final String UNITS = "Units"; + public static final String INDEX = "Index"; + public static final String MINIMUM = "Minimum"; + public static final String MAXIMUM = "Maximum"; + public static final String DEFAULT = "Default"; + public static final String REQUIRED = "Required"; + public static final String SEARCHHELPID = "SearchHelpID"; + public static final String UNIQUE = "Unique"; + public static final String MODTIMESTAMP = "ModTimeStamp"; + public static final String MODTIMESTAMPNAME = "ModTimeStampName"; + public static final String FOREIGNKEYNAME = "ForeignKeyName"; + public static final String FOREIGNFIELD = "ForeignField"; + public static final String INKEYINDEX = "InKeyIndex"; + public static final String KEYQUERY = "KeyQuery"; + public static final String KEYSELECT = "KeySelect"; + + private static final String[] DATATYPES = "Boolean,Character,Date,DateTime,Time,Tiny,Small,Int,Long,Decimal".split(","); + private static final AttrType sDataTypes = new AttrEnum(DATATYPES); + private static final String[] INTERPRETATIONS = "Number,Currency,Lookup,LookupMulti,LookupBitstring,LookupBitmask".split(","); + private static final AttrType sInterpretations = new AttrEnum(INTERPRETATIONS); + private static final String[] ALIGNMENTS = "Left,Right,Center,Justify".split(","); + private static final AttrType sAlignments = new AttrEnum(ALIGNMENTS); + private static final String[] UNITSS = "Feet,Meters,SqFt,SqMeters,Acres,Hectares".split(","); + private static final AttrType sUnits = new AttrEnum(UNITSS); + + public MTable() { + this(DEFAULT_PARSING); + } + + public MTable(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + + String metadataEntryID = getStringAttribute(METADATAENTRYID); + if (metadataEntryID == null){ + metadataEntryID = this.getSystemName(); + } + return metadataEntryID; + } + + public String getSystemName() { + return getStringAttribute(SYSTEMNAME); + } + + public String getStandardName() { + return getStringAttribute(STANDARDNAME); + } + + public String getLongName() { + return getStringAttribute(LONGNAME); + } + + public String getDBName() { + return getStringAttribute(DBNAME); + } + + public String getShortName() { + return getStringAttribute(SHORTNAME); + } + + public int getMaximumLength() { + return getIntAttribute(MAXIMUMLENGTH); + } + + public String getDataType() { + return getStringAttribute(DATATYPE); + } + + public int getPrecision() { + return getIntAttribute(PRECISION); + } + + public boolean getSearchable() { + return getBooleanAttribute(SEARCHABLE); + } + + public String getInterpretation() { + return getStringAttribute(INTERPRETATION); + } + + public boolean isLookup() { + String interp = getInterpretation(); + if (interp != null && interp.startsWith("Lookup")) { + return true; + } + if (getSystemName().equalsIgnoreCase("status")) { + System.out.println("Field is " + getSystemName() + " and interp " + "is " + interp + + " but isLookup() is false"); + } + return false; + } + + public String getAlignment() { + return getStringAttribute(ALIGNMENT); + } + + public boolean getUseSeparator() { + return getBooleanAttribute(USESEPARATOR); + } + + public String getEditMaskID() { + return getStringAttribute(EDITMASKID); + } + + public String getLookupName() { + return getStringAttribute(LOOKUPNAME); + } + + public int getMaxSelect() { + return getIntAttribute(MAXSELECT); + } + + public String getUnits() { + return getStringAttribute(UNITS); + } + + public int getIndex() { + return getIntAttribute(INDEX); + } + + public int getMinimum() { + return getIntAttribute(MINIMUM); + } + + public int getMaximum() { + return getIntAttribute(MAXIMUM); + } + + public int getDefault() { + return getIntAttribute(DEFAULT); + } + + public int getRequired() { + return getIntAttribute(REQUIRED); + } + + public String getSearchHelpID() { + return getStringAttribute(SEARCHHELPID); + } + + public boolean getUnique() { + return getBooleanAttribute(UNIQUE); + } + + public boolean getModTimestamp() { + return getBooleanAttribute(MODTIMESTAMP); + } + + public boolean getModTimestampName() { + return getBooleanAttribute(MODTIMESTAMPNAME); + } + + public boolean getInKeyIndex() { + return getBooleanAttribute(INKEYINDEX); + } + + @Override + public MetadataType[] getChildTypes() { + return sNoChildren; + } + + @Override + protected String getIdAttr() { + return SYSTEMNAME; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, retsid); + attributeMap.put(SYSTEMNAME, retsname); + attributeMap.put(STANDARDNAME, retsname); + attributeMap.put(LONGNAME, sText256); + attributeMap.put(DBNAME, sAlphanum10); + attributeMap.put(SHORTNAME, sText64); + attributeMap.put(MAXIMUMLENGTH, sAttrNumericPositive); + attributeMap.put(DATATYPE, sDataTypes); + attributeMap.put(PRECISION, sAttrNumeric); + attributeMap.put(SEARCHABLE, sAttrBoolean); + attributeMap.put(INTERPRETATION, sInterpretations); + attributeMap.put(ALIGNMENT, sAlignments); + attributeMap.put(USESEPARATOR, sAttrBoolean); + // XXX: but multiples are separated by commas + attributeMap.put(EDITMASKID, retsname); + attributeMap.put(LOOKUPNAME, retsname); + attributeMap.put(MAXSELECT, sAttrNumeric); + attributeMap.put(UNITS, sUnits); + attributeMap.put(INDEX, sAttrNumeric); + attributeMap.put(MINIMUM, sAttrNumeric); + attributeMap.put(MAXIMUM, sAttrNumeric); + // XXX: serial + attributeMap.put(DEFAULT, sAttrNumeric); + attributeMap.put(REQUIRED, sAttrNumeric); + attributeMap.put(SEARCHHELPID, retsname); + attributeMap.put(UNIQUE, sAttrBoolean); + attributeMap.put(MODTIMESTAMP, sAttrBoolean); + attributeMap.put(MODTIMESTAMPNAME, retsname); + attributeMap.put(FOREIGNKEYNAME,retsid); + attributeMap.put(FOREIGNFIELD,retsname); + attributeMap.put(INKEYINDEX, sAttrBoolean); + + attributeMap.put(KEYQUERY, sAttrBoolean); + attributeMap.put(KEYSELECT, sAttrBoolean); + } + + public String getForeignKeyName() { + return getStringAttribute(FOREIGNKEYNAME); + } + + public String getForeignField() { + return getStringAttribute(FOREIGNFIELD); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MUpdate.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MUpdate.java new file mode 100644 index 0000000..d20acbf --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MUpdate.java @@ -0,0 +1,91 @@ +package com.ossez.usreio.tests.common.metadata.types; + +//import java.util.Date; +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MUpdate extends MetaObject { + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String UPDATENAME = "UpdateName"; + public static final String DESCRIPTION = "Description"; + public static final String KEYFIELD = "KeyField"; + public static final String VERSION = "Version"; + public static final String DATE = "Date"; + public static final String UPDATETYPEVERSION = "UpdateTypeVersion"; + public static final String UPDATETYPEDATE = "UpdateTypeDate"; + + public MUpdate() { + this(DEFAULT_PARSING); + } + + public MUpdate(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getUpdateName() { + return getStringAttribute(UPDATENAME); + } + + public String getDescription() { + return getStringAttribute(DESCRIPTION); + } + + public String getKeyField() { + return getStringAttribute(KEYFIELD); + } + + public int getVersion() { + int v = getIntAttribute(VERSION); + if (v == 0){ + v = getIntAttribute(UPDATETYPEVERSION); + } + return v; + } + + public String getDate() { + String d = getDateAttribute(DATE); + if (d == null ){ + d = getDateAttribute(UPDATETYPEDATE); + } + return d; + } + + public MUpdateType getMUpdateType(String systemName) { + return (MUpdateType) getChild(MetadataType.UPDATE_TYPE, systemName); + } + + public MUpdateType[] getMUpdateTypes() { + MUpdateType[] tmpl = new MUpdateType[0]; + return (MUpdateType[]) getChildren(MetadataType.UPDATE_TYPE).toArray(tmpl); + } + + @Override + public MetadataType[] getChildTypes() { + return sTypes; + } + + @Override + protected String getIdAttr() { + return UPDATENAME; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAttrMetadataEntryId); + attributeMap.put(UPDATENAME, sAlphanum24); + attributeMap.put(DESCRIPTION, sPlaintext64); + attributeMap.put(KEYFIELD, sAlphanum32); + attributeMap.put(VERSION, sAttrVersion); + attributeMap.put(DATE, sAttrDate); + attributeMap.put(UPDATETYPEVERSION, sAttrVersion); + attributeMap.put(UPDATETYPEDATE, sAttrDate); + } + + private static final MetadataType[] sTypes = { MetadataType.UPDATE_TYPE }; +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MUpdateHelp.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MUpdateHelp.java new file mode 100644 index 0000000..23d7c52 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MUpdateHelp.java @@ -0,0 +1,50 @@ +package com.ossez.usreio.tests.common.metadata.types; + +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MUpdateHelp extends MetaObject { + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String UPDATEHELPID = "UpdateHelpID"; + public static final String VALUE = "Value"; + + public MUpdateHelp() { + this(DEFAULT_PARSING); + } + + public MUpdateHelp(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getUpdateHelpID() { + return getStringAttribute(UPDATEHELPID); + } + + public String getValue() { + return getStringAttribute(VALUE); + } + + @Override + public MetadataType[] getChildTypes() { + return sNoChildren; + } + + @Override + protected String getIdAttr() { + return UPDATEHELPID; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAttrMetadataEntryId); + attributeMap.put(UPDATEHELPID, sAlphanum32); + attributeMap.put(VALUE, sText1024); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MUpdateType.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MUpdateType.java new file mode 100644 index 0000000..38ab5f2 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MUpdateType.java @@ -0,0 +1,103 @@ +package com.ossez.usreio.tests.common.metadata.types; + +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.AttrType; +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; +import com.ossez.usreio.tests.common.metadata.attrib.AttrGenericText; + +public class MUpdateType extends MetaObject { + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String SYSTEMNAME = "SystemName"; + public static final String SEQUENCE = "Sequence"; + public static final String ATTRIBUTES = "Attributes"; + public static final String DEFAULT = "Default"; + public static final String VALIDATIONEXPRESSIONID = "ValidationExpressionID"; + public static final String UPDATEHELPID = "UpdateHelpID"; + public static final String VALIDATIONLOOKUPNAME = "ValidationLookupName"; + public static final String VALIDATIONEXTERNALNAME = "ValidationExternalName"; + public static final String MAXCHOICE = "MaxChoice"; + public static final String MAXUPDATE = "MaxUpdate"; + + private static final AttrType sAttributes = new AttrGenericText(0, 10, "12345,"); + + public MUpdateType() { + this(DEFAULT_PARSING); + + } + + public MUpdateType(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getSystemName() { + return getStringAttribute(SYSTEMNAME); + } + + public int getSequence() { + return getIntAttribute(SEQUENCE); + } + + public String getAttributes() { + return getStringAttribute(ATTRIBUTES); + } + + public String getDefault() { + return getStringAttribute(DEFAULT); + } + + public String getValidationExpressionID() { + return getStringAttribute(VALIDATIONEXPRESSIONID); + } + + public String getUpdateHelpID() { + return getStringAttribute(UPDATEHELPID); + } + + public String getValidationLookupName() { + return getStringAttribute(VALIDATIONLOOKUPNAME); + } + + public String getValidationExternalName() { + return getStringAttribute(VALIDATIONEXTERNALNAME); + } + + public int getMaxChoice() { + return getIntAttribute(MAXCHOICE); + } + + public int getMaxUpdate() { + return getIntAttribute(MAXUPDATE); + } + + @Override + public MetadataType[] getChildTypes() { + return sNoChildren; + } + + @Override + protected String getIdAttr() { + return SYSTEMNAME; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAttrMetadataEntryId); + attributeMap.put(SYSTEMNAME, sAlphanum32); + attributeMap.put(SEQUENCE, sAttrNumeric); + attributeMap.put(ATTRIBUTES, sAttributes); + attributeMap.put(DEFAULT, sPlaintext); + attributeMap.put(VALIDATIONEXPRESSIONID, sAlphanum32); + attributeMap.put(UPDATEHELPID, sAlphanum32); + attributeMap.put(VALIDATIONLOOKUPNAME, sAlphanum32); + attributeMap.put(VALIDATIONEXTERNALNAME, sAlphanum32); + attributeMap.put(MAXCHOICE, sAttrNumeric); + attributeMap.put(MAXUPDATE, sAttrNumeric); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationExpression.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationExpression.java new file mode 100644 index 0000000..4e62748 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationExpression.java @@ -0,0 +1,61 @@ +package com.ossez.usreio.tests.common.metadata.types; + +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.AttrType; +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; +import com.ossez.usreio.tests.common.metadata.attrib.AttrEnum; + +public class MValidationExpression extends MetaObject { + + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String VALIDATIONEXPRESSIONID = "ValidationExpressionID"; + public static final String VALIDATIONEXPRESSIONTYPE = "ValidationExpressionType"; + public static final String VALUE = "Value"; + private static final String[] VALIDATIONEXPRESSIONTYPES = "ACCEPT,REJECT,SET".split(","); + private static final AttrType sExpressionType = new AttrEnum(VALIDATIONEXPRESSIONTYPES); + + public MValidationExpression() { + this(DEFAULT_PARSING); + } + + public MValidationExpression(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getValidationExpressionID() { + return getStringAttribute(VALIDATIONEXPRESSIONID); + } + + public String getValidationExpressionType() { + return getStringAttribute(VALIDATIONEXPRESSIONTYPE); + } + + public String getValue() { + return getStringAttribute(VALUE); + } + + @Override + public MetadataType[] getChildTypes() { + return sNoChildren; + } + + @Override + protected String getIdAttr() { + return VALIDATIONEXPRESSIONID; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAttrMetadataEntryId); + attributeMap.put(VALIDATIONEXPRESSIONID, sAlphanum32); + attributeMap.put(VALIDATIONEXPRESSIONTYPE, sExpressionType); + attributeMap.put(VALUE, sText512); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationExternal.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationExternal.java new file mode 100644 index 0000000..2d266f5 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationExternal.java @@ -0,0 +1,76 @@ +package com.ossez.usreio.tests.common.metadata.types; + +//import java.util.Date; +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MValidationExternal extends MetaObject { + + private static final MetadataType[] CHILDREN = { MetadataType.VALIDATION_EXTERNAL_TYPE }; + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String VALIDATIONEXTERNALNAME = "ValidationExternalName"; + public static final String SEARCHRESOURCE = "SearchResource"; + public static final String SEARCHCLASS = "SearchClass"; + public static final String VERSION = "Version"; + public static final String DATE = "Date"; + + public MValidationExternal() { + this(DEFAULT_PARSING); + } + + public MValidationExternal(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getValidationExternalName() { + return getStringAttribute(VALIDATIONEXTERNALNAME); + } + + public String getSearchResource() { + return getStringAttribute(SEARCHRESOURCE); + } + + public String getSearchClass() { + return getStringAttribute(SEARCHCLASS); + } + + public int getVersion() { + return getIntAttribute(VERSION); + } + + public String getDate() { + return getDateAttribute(DATE); + } + + public MValidationExternalType[] getMValidationExternalTypes() { + MValidationExternalType[] tmpl = new MValidationExternalType[0]; + return (MValidationExternalType[]) getChildren(MetadataType.VALIDATION_EXTERNAL_TYPE).toArray(tmpl); + } + + @Override + public MetadataType[] getChildTypes() { + return CHILDREN; + } + + @Override + protected String getIdAttr() { + return VALIDATIONEXTERNALNAME; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAttrMetadataEntryId); + attributeMap.put(VALIDATIONEXTERNALNAME, sAlphanum32); + attributeMap.put(SEARCHRESOURCE, sAlphanum32); + attributeMap.put(SEARCHCLASS, sAlphanum32); + attributeMap.put(VERSION, sAttrVersion); + attributeMap.put(DATE, sAttrDate); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationExternalType.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationExternalType.java new file mode 100644 index 0000000..dc26da1 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationExternalType.java @@ -0,0 +1,57 @@ +package com.ossez.usreio.tests.common.metadata.types; + +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MValidationExternalType extends MetaObject { + + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String SEARCHFIELD = "SearchField"; + public static final String DISPLAYFIELD = "DisplayField"; + public static final String RESULTFIELDS = "ResultFields"; + + public MValidationExternalType() { + this(DEFAULT_PARSING); + } + + public MValidationExternalType(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getSearchField() { + return getStringAttribute(SEARCHFIELD); + } + + public String getDisplayField() { + return getStringAttribute(DISPLAYFIELD); + } + + public String getResultFields() { + return getStringAttribute(RESULTFIELDS); + } + + @Override + public MetadataType[] getChildTypes() { + return sNoChildren; + } + + @Override + protected String getIdAttr() { + return null; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAttrMetadataEntryId); + attributeMap.put(SEARCHFIELD, sPlaintext512); + attributeMap.put(DISPLAYFIELD, sPlaintext512); + attributeMap.put(RESULTFIELDS, sPlaintext1024); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationLookup.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationLookup.java new file mode 100644 index 0000000..118dca1 --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationLookup.java @@ -0,0 +1,76 @@ +package com.ossez.usreio.tests.common.metadata.types; + +//import java.util.Date; +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MValidationLookup extends MetaObject { + + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String VALIDATIONLOOKUPNAME = "ValidationLookupName"; + public static final String PARENT1FIELD = "Parent1Field"; + public static final String PARENT2FIELD = "Parent2Field"; + public static final String VERSION = "Version"; + public static final String DATE = "Date"; + private static final MetadataType[] sChildren = { MetadataType.VALIDATION_LOOKUP_TYPE }; + + public MValidationLookup() { + this(DEFAULT_PARSING); + } + + public MValidationLookup(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getValidationLookupName() { + return getStringAttribute(VALIDATIONLOOKUPNAME); + } + + public String getParent1Field() { + return getStringAttribute(PARENT1FIELD); + } + + public String getParent2Field() { + return getStringAttribute(PARENT2FIELD); + } + + public int getVersion() { + return getIntAttribute(VERSION); + } + + public String getDate() { + return getDateAttribute(DATE); + } + + @Override + public MetadataType[] getChildTypes() { + return sChildren; + } + + @Override + protected String getIdAttr() { + return VALIDATIONLOOKUPNAME; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAttrMetadataEntryId); + attributeMap.put(VALIDATIONLOOKUPNAME, sAlphanum32); + attributeMap.put(PARENT1FIELD, sAlphanum32); + attributeMap.put(PARENT2FIELD, sAlphanum32); + attributeMap.put(VERSION, sAttrVersion); + attributeMap.put(DATE, sAttrDate); + } + + public MValidationLookupType[] getMValidationLookupTypes() { + MValidationLookupType[] tmpl = new MValidationLookupType[0]; + return (MValidationLookupType[]) getChildren(MetadataType.VALIDATION_LOOKUP_TYPE).toArray(tmpl); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationLookupType.java b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationLookupType.java new file mode 100644 index 0000000..ff3e60f --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/tests/common/metadata/types/MValidationLookupType.java @@ -0,0 +1,56 @@ +package com.ossez.usreio.tests.common.metadata.types; + +import java.util.Map; + +import com.ossez.usreio.tests.common.metadata.MetaObject; +import com.ossez.usreio.tests.common.metadata.MetadataType; + +public class MValidationLookupType extends MetaObject { + public static final String METADATAENTRYID = "MetadataEntryID"; + public static final String VALIDTEXT = "ValidText"; + public static final String PARENT1VALUE = "Parent1Value"; + public static final String PARENT2VALUE = "Parent2Value"; + + public MValidationLookupType() { + this(DEFAULT_PARSING); + } + + public MValidationLookupType(boolean strictParsing) { + super(strictParsing); + } + + public String getMetadataEntryID() { + return getStringAttribute(METADATAENTRYID); + } + + public String getValidText() { + return getStringAttribute(VALIDTEXT); + } + + public String getParent1Value() { + return getStringAttribute(PARENT1VALUE); + } + + public String getParent2Value() { + return getStringAttribute(PARENT2VALUE); + } + + @Override + public MetadataType[] getChildTypes() { + return sNoChildren; + } + + @Override + protected String getIdAttr() { + return null; + } + + @Override + protected void addAttributesToMap(Map attributeMap) { + attributeMap.put(METADATAENTRYID, sAttrMetadataEntryId); + attributeMap.put(VALIDTEXT, sAlphanum32); + attributeMap.put(PARENT1VALUE, sAlphanum32); + attributeMap.put(PARENT2VALUE, sAlphanum32); + } + +} diff --git a/rets-io-client/src/main/java/com/ossez/usreio/util/SessionUtils.java b/rets-io-client/src/main/java/com/ossez/usreio/util/SessionUtils.java new file mode 100644 index 0000000..2305a3f --- /dev/null +++ b/rets-io-client/src/main/java/com/ossez/usreio/util/SessionUtils.java @@ -0,0 +1,62 @@ +package com.ossez.usreio.util; + +import com.ossez.usreio.client.*; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SessionUtils for RETS server session + * + * @author YuCheng Hu + */ +public final class SessionUtils { + private static final Logger logger = LoggerFactory.getLogger(SessionUtils.class); + + // Prevent the class from being constructed + private SessionUtils() { + + } + + /** + * Login to Server and return session Object + * + * @param retsLoginUrl + * @param retsUsername + * @param retsPassword + * @return + */ + public static RetsSession retsLogin(String retsLoginUrl, String retsUsername, String retsPassword, RetsVersion retsVersion) throws RetsException { + logger.debug("RETS Session Login URL: [{}]", retsLoginUrl); + + LoginResponse loginResponse = new LoginResponse(); + + //Create a RetsHttpClient (other constructors provide configuration i.e. timeout, gzip capability) + RetsHttpClient httpClient = new CommonsHttpClient(); + + // SET RETS VERSION + if (ObjectUtils.isEmpty(retsVersion)) + retsVersion = RetsVersion.DEFAULT; + + //Create a RetesSession with RetsHttpClient + RetsSession session = new RetsSession(retsLoginUrl, httpClient, retsVersion); + + //Set method as GET or POST + session.setMethod("POST"); + try { + //Login + loginResponse = session.login(retsUsername, retsPassword); + } catch (RetsException ex) { + throw ex; + } + + // SESSION NULL CHECK + if (!(!ObjectUtils.isEmpty(session) && StringUtils.isNotEmpty(loginResponse.getSessionId()))) { + session = null; + } + + logger.info("Session ID :[{}]", loginResponse.getSessionId()); + return session; + } +} diff --git a/rets-io-client/src/main/resources/build.xml b/rets-io-client/src/main/resources/build.xml new file mode 100644 index 0000000..3ea8e25 --- /dev/null +++ b/rets-io-client/src/main/resources/build.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rets-io-client/src/main/resources/default.properties b/rets-io-client/src/main/resources/default.properties new file mode 100644 index 0000000..4d736e2 --- /dev/null +++ b/rets-io-client/src/main/resources/default.properties @@ -0,0 +1 @@ +# required for build diff --git a/rets-io-client/src/main/resources/dummy.dtd b/rets-io-client/src/main/resources/dummy.dtd new file mode 100644 index 0000000..9a6a4ec --- /dev/null +++ b/rets-io-client/src/main/resources/dummy.dtd @@ -0,0 +1,3 @@ +DO NOT REMOVE! +This file is search for as a resource. +All other DTDs should be in the same directory as this file. diff --git a/rets-io-client/src/main/resources/log4j.properties b/rets-io-client/src/main/resources/log4j.properties new file mode 100644 index 0000000..a4e6cd2 --- /dev/null +++ b/rets-io-client/src/main/resources/log4j.properties @@ -0,0 +1,38 @@ +log4j.rootCategory=error, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n + + +# client API logfile +log4j.appender.R1=org.apache.log4j.RollingFileAppender +log4j.appender.R1.File=/tmp/retsClientAPI.log +log4j.appender.R1.MaxFileSize=100KB +log4j.appender.R1.MaxBackupIndex=3 +log4j.appender.R1.layout=org.apache.log4j.PatternLayout +log4j.appender.R1.layout.ConversionPattern=%p %t %c CLIENTAPI- %m%n + + +# server logfile +log4j.appender.R2=org.apache.log4j.RollingFileAppender +log4j.appender.R2.File=/tmp/retsServer.log +log4j.appender.R2.MaxFileSize=100KB +log4j.appender.R2.MaxBackupIndex=3 +log4j.appender.R2.layout=org.apache.log4j.PatternLayout +log4j.appender.R2.layout.ConversionPattern=%p %t %c SERVER - %m%n + +# setup RETS client API logging +log4j.category.org.realtor.rets.retsapi=debug, R1 +log4j.category.org.realtor.rets.util=debug, R1 + + +# setup RETS server logging +log4j.category.org.realtor.rets.server=debug, R2, stdout +log4j.category.org.realtor.rets.util=debug, R2 +log4j.category.org.realtor.rets.persistance=error, R2 + + +log4j.additivity.org.realtor.rets.server=false +log4j.additivity.org.realtor.rets.retsapi=false +log4j.additivity.org.realtor.rets.util=false diff --git a/rets-io-client/src/main/resources/log4j2.xml b/rets-io-client/src/main/resources/log4j2.xml new file mode 100644 index 0000000..ff28157 --- /dev/null +++ b/rets-io-client/src/main/resources/log4j2.xml @@ -0,0 +1,97 @@ + + + + + + /home/logs/reoc/services/ + + + + + + + + + + + + + + + + + + %-d{yyyy/MM/dd HH:mm:ss} %-5p [%c] - %m%n + + + + + + + + + + + + + + + %-d{yyyy/MM/dd HH:mm:ss} %-5p [%c] - %m%n + + + + + + + + + + + + + + + %-d{yyyy/MM/dd HH:mm:ss} %-5p [%c] - %m%n + + + + + + + + + + + + + + + %-d{yyyy/MM/dd HH:mm:ss} %-5p [%c] - %m%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rets-io-client/src/main/resources/logback.xml b/rets-io-client/src/main/resources/logback.xml new file mode 100644 index 0000000..8599e4f --- /dev/null +++ b/rets-io-client/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/rets-io-client/src/main/resources/metadata_tables.xml b/rets-io-client/src/main/resources/metadata_tables.xml new file mode 100644 index 0000000..bc735b3 --- /dev/null +++ b/rets-io-client/src/main/resources/metadata_tables.xml @@ -0,0 +1,41 @@ + +
+ +
+
+ +
+
+
+ + + +
+
+
+
+
+
+ + +
+
+ + +
+
+ +
+ +
diff --git a/rets-io-client/src/main/resources/templates.xml b/rets-io-client/src/main/resources/templates.xml new file mode 100644 index 0000000..8896c1d --- /dev/null +++ b/rets-io-client/src/main/resources/templates.xml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/AllTests.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/AllTests.java new file mode 100644 index 0000000..1a9bd6c --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/AllTests.java @@ -0,0 +1,31 @@ +package com.ossez.usreio.tests.client; + +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * A suite of all tests in the org.realtors.rets.client.* package. + */ +public class AllTests { + /** + * Returns a test suite for all classes in org.realtors.rets.client.*. + */ + public static Test suite() { + TestSuite suite; + + suite = new TestSuite(); + /*suite.addTestSuite(GetMetadataRequestTest.class); + suite.addTestSuite(GetMetadataResponseTest.class); + suite.addTestSuite(GetObjectResponseIteratorTest.class); + suite.addTestSuite(LoginRequestTest.class); + suite.addTestSuite(LoginResponseTest.class); + suite.addTestSuite(LogoutResponseTest.class); + // suite.addTestSuite(MetadataTableTest.class); + // suite.addTestSuite(MetadataTableBuilderTest.class); + suite.addTestSuite(RetsVersionTest.class); + suite.addTestSuite(SearchResultImplTest.class); + suite.addTestSuite(SearchResultHandlerTest.class); + suite.addTestSuite(SingleObjectResponseTest.class);*/ + return suite; + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/ConnectionTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/ConnectionTest.java new file mode 100644 index 0000000..446fa38 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/ConnectionTest.java @@ -0,0 +1,52 @@ +package com.ossez.usreio.tests.client; + +import org.junit.jupiter.api.Test; +import com.ossez.usreio.client.retsapi.RETSConnection; +import com.ossez.usreio.client.retsapi.RETSLoginTransaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import java.io.InputStream; +import java.util.Properties; + +/** + * @author YuCheng + */ +public class ConnectionTest { + + private final static Logger logger = LoggerFactory.getLogger(ConnectionTest.class); + + /** + * Do RetsServerConnection Test + */ + @Test + public void testStaticVariableChange() { + + // BasicConfigurator.configure(); + + RETSConnection rc = new RETSConnection(); + RETSLoginTransaction trans = new RETSLoginTransaction(); + + try { + Properties props = new Properties(); + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + InputStream inputStream = loader.getResourceAsStream("rets.properties"); + + props.load(inputStream); + + // Add the optional request parameters if they exist, are non-null and non-zero-length + // rc.setRequestHeaderField("Authorization", (String)props.get("login.AUTHORIZATION")); + rc.setServerUrl((String) props.getProperty("rets_server")); + trans.setUrl((String) props.getProperty("rets_server")); + trans.setUsername((String) props.getProperty("rets_username")); + trans.setPassword((String) props.getProperty("rets_password")); + } catch (Exception e) { + e.printStackTrace(); + } + + rc.execute(trans); + + } + +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/GetMetadataRequestTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/GetMetadataRequestTest.java new file mode 100644 index 0000000..3a2f735 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/GetMetadataRequestTest.java @@ -0,0 +1,58 @@ +package com.ossez.usreio.tests.client; + +import com.ossez.usreio.client.GetMetadataRequest; +import com.ossez.usreio.client.InvalidArgumentException; +import com.ossez.usreio.client.RetsException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class GetMetadataRequestTest extends RetsTestCase { + public void testGetMetadataRequestSimple() throws RetsException { + GetMetadataRequest request = new GetMetadataRequest("SYSTEM", "*"); + request.setUrl("http://rets.test:6103/getMetadata"); + assertFalse(request.isCompactFormat()); + assertTrue(request.isStandardXmlFormat()); +// assertNull(request.getStandardXmlVersion()); +// assertEquals("http://rets.test:6103/getMetadata", request.getUrl()); +// assertEquals("Format=STANDARD-XML&ID=*&Type=METADATA-SYSTEM", RetsUtil.urlDecode(request.getHttpParameters())); + } + + public void testGetMetadataRequestMultipleIds() throws RetsException { + GetMetadataRequest request = new GetMetadataRequest("UPDATE_TYPE", new String[] { "ActiveAgent", "ACTAGT", + "Change_ACTAGT" }); + request.setCompactFormat(); + + assertTrue(request.isCompactFormat()); + assertFalse(request.isStandardXmlFormat()); +// assertEquals("Format=COMPACT&ID=ActiveAgent:ACTAGT:Change_ACTAGT" + "&Type=METADATA-UPDATE_TYPE", RetsUtil +// .urlDecode(request.getHttpParameters())); + } + + public void testInvalidGetMetadataRequests() throws RetsException { + try { + // ID for METADATA-SYSTEM can only be 0 or * + new GetMetadataRequest("SYSTEM", "Blah"); +// fail("Should have thrown an InvalidArgumentException"); + } catch (InvalidArgumentException e) { + // Expected + } + + try { + // ID for METADATA-RESOURCE can only be 0 or * + new GetMetadataRequest("RESOURCE", "Blah"); +// fail("Should have thrown an InvalidArgumentException"); + } catch (InvalidArgumentException e) { + // Expected + } + + try { + // Must have at least 1 ID + new GetMetadataRequest("RESOURCE", new String[0]); +// fail("Should have thrown an InvalidArgumentException"); + } catch (InvalidArgumentException e) { + // Expected + } + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/GetMetadataResponseTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/GetMetadataResponseTest.java new file mode 100644 index 0000000..d3491b4 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/GetMetadataResponseTest.java @@ -0,0 +1,156 @@ +package com.ossez.usreio.tests.client; + +//import java.util.List; +//import java.util.ArrayList; +//import java.util.Iterator; +//import java.util.Map; + +public class GetMetadataResponseTest extends RetsTestCase { + public void testValidSystemMetadataResponse() { + // GetMetadataResponse response = new GetMetadataResponse( + // getResource("getMetadataResponse_system.xml")); + + // MetadataSegment[] segments = response.getMetadataSegments(); + // assertEquals(1, segments.length); + // MetadataSegment segment = segments[0]; + // assertEquals("SYSTEM", segment.getName()); + // assertEquals("01.00.001", segment.getAttribute("Version")); + // assertEquals("Tue, 27 May 2003 12:00:00 CDT", + // segment.getAttribute("Date")); + // assertEquals("CRT_RETS", segment.getSystemId()); + // assertEquals("Center for REALTOR Technology", + // segment.getSystemDescription()); + // assertEquals("The reference implementation of a RETS Server", + // segment.getSystemComments()); + // assertNull(segment.getColumns()); + // assertNull(segment.getData()); + } + + public void testSingleSegmentResponse() { + // GetMetadataResponse response = new GetMetadataResponse( + // getResource("getMetadataResponse_updateType.xml")); + // + // MetadataSegment[] segments = response.getMetadataSegments(); + // assertEquals(1, segments.length); + // MetadataSegment segment = segments[0]; + // assertEquals("UPDATE_TYPE", segment.getName()); + // + // assertEquals("ActiveAgent", segment.getAttribute("resource")); + // assertEquals("ACTAGT", segment.getAttribute("CLASS")); + // assertEquals("Change_ACTAGT", segment.getAttribute("update")); + // assertEquals("1.00.000", segment.getAttribute("VERSION")); + // assertEquals("Sat, 20 Mar 2002 12:03:38 GMT", + // segment.getAttribute("date")); + // + // // Try with the opposite case above to check case-insensitive + // // comparisons + // Map attributes = segment.getAttributes(); + // assertEquals(5, attributes.size()); + // assertEquals("ActiveAgent", attributes.get("RESOURCE")); + // assertEquals("ACTAGT", attributes.get("class")); + // assertEquals("Change_ACTAGT", attributes.get("UPDATE")); + // assertEquals("1.00.000", attributes.get("version")); + // assertEquals("Sat, 20 Mar 2002 12:03:38 GMT", + // attributes.get("DATE")); + // + // assertNull(segment.getSystemId()); + // assertNull(segment.getSystemDescription()); + // assertNull(segment.getSystemComments()); + // + // List columns = segment.getColumns(); + // assertNotNull("columns not null", columns); + // assertEquals("columns.size", 8, columns.size()); + // + // List expectedColumns = new ArrayList(); + // expectedColumns.add("SystemName"); + // expectedColumns.add("Sequence"); + // expectedColumns.add("Attributes"); + // expectedColumns.add("Default"); + // expectedColumns.add("ValidationExpressionID"); + // expectedColumns.add("UpdateHelpID"); + // expectedColumns.add("ValidationLookupName"); + // expectedColumns.add("ValidationExternalName"); + // assertEquals("columns", expectedColumns, columns); + // + // List data = segment.getData(); + // assertNotNull(data); + // assertEquals(2, data.size()); + // Iterator i = data.iterator(); + // + // assertTrue(i.hasNext()); + // List dataRow = (List) i.next(); + // assertEquals(8, dataRow.size()); + // List expectedDataRow = new ArrayList(); + // expectedDataRow.add("AGENT_ID"); + // expectedDataRow.add("1"); + // expectedDataRow.add("1"); + // expectedDataRow.add("0"); + // expectedDataRow.add(null); + // expectedDataRow.add(null); + // expectedDataRow.add(null); + // expectedDataRow.add(null); + // assertEquals(expectedDataRow, dataRow); + // + // assertTrue(i.hasNext()); + // dataRow = (List) i.next(); + // assertEquals(8, dataRow.size()); + // expectedDataRow = new ArrayList(); + // expectedDataRow.add("OFFICE_ID"); + // expectedDataRow.add("2"); + // expectedDataRow.add("2"); + // expectedDataRow.add("0"); + // expectedDataRow.add(null); + // expectedDataRow.add(null); + // expectedDataRow.add(null); + // expectedDataRow.add(null); + // assertEquals(expectedDataRow, dataRow); + } + + public void testMultipleSegmentResponse() { + // GetMetadataResponse response = new GetMetadataResponse( + // getResource("getMetadataResponse_lookupZero.xml")); + // + // MetadataSegment[] segments = response.getMetadataSegments(); + // assertEquals(2, segments.length); + // + // // Check first segment + // MetadataSegment segment = segments[0]; + // assertEquals("LOOKUP", segment.getName()); + // assertEquals("Property", segment.getAttribute("Resource")); + // List columns = segment.getColumns(); + // assertNotNull(columns); + // assertEquals(4, columns.size()); + // + // List expectedColumns = new ArrayList(); + // expectedColumns.add("LookupName"); + // expectedColumns.add("VisibleName"); + // expectedColumns.add("Version"); + // expectedColumns.add("Date"); + // assertEquals("columns", expectedColumns, columns); + // + // List data = segment.getData(); + // assertNotNull(data); + // assertEquals(9, data.size()); + // + // // Check second segment + // segment = segments[1]; + // assertEquals("LOOKUP", segment.getName()); + // assertEquals("Agent", segment.getAttribute("Resource")); + // columns = segment.getColumns(); + // assertNotNull(columns); + // assertEquals(4, columns.size()); + // assertEquals("columns", expectedColumns, columns); + // + // data = segment.getData(); + // assertNotNull(data); + // assertEquals(1, data.size()); + } + + public void testNoRecordsMetadataResponse() { + // GetMetadataResponse response = new GetMetadataResponse( + // getResource("getMetadataResponse_noRecords.xml")); + // + // MetadataSegment[] segments = response.getMetadataSegments(); + // assertEquals(0, segments.length); + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/GetObjectResponseIteratorTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/GetObjectResponseIteratorTest.java new file mode 100644 index 0000000..b06f01f --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/GetObjectResponseIteratorTest.java @@ -0,0 +1,194 @@ +package com.ossez.usreio.tests.client; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import com.ossez.usreio.client.GetObjectIterator; +import com.ossez.usreio.client.GetObjectResponse; +import com.ossez.usreio.client.SingleObjectResponse; +import junit.framework.TestCase; + +public class GetObjectResponseIteratorTest extends TestCase { + private static final String BOUNDARY = "jack"; + + private static final String BINARY_BLOB_1 = "1)dcg fa8 5 uiwjskdgsdfkg hdsfa bdf" + " erkfjhwfewuhuh" + + "B\r\n\r\n"; + + private static final String BINARY_BLOB_2 = "2)dcg fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" + "\tAAAAAAAAAAAAhdsfa bdf"; + + private static final String BINARY_BLOB_3 = "3)dcg fAAAAAAAAAAAAAAAAA\t\\!" + + "\r\n\r\nAAAAAAAAAAAAAAAAAAAAAAAAAhdsfa bdf"; + + private static final String BINARY_BLOB_4 = "fgsdgsdfg"; + + private static final String BINARY_BLOB_5 = "4)dcg fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhdsfa bdf"; + + public static final byte[] MULTIPART_RESPONSE_BODY = ("--" + BOUNDARY + "\r\n" + "Content-Type: text\r\n" + + "Content-ID: one\r\n" + "Object-ID: 1\r\n" + "\r\n" + BINARY_BLOB_1 + "\r\n--" + BOUNDARY + "\r\n" + + "Content-Type: gray-matter\r\n" + "Content-ID: two\r\n" + "Object-ID: 2\r\n" + "\r\n" + BINARY_BLOB_2 + + "\r\n--" + BOUNDARY + "\r\n" + "Content-Type: blue-matter\r\n" + "Content-ID: three\r\n" + + "Object-ID: 3\r\n" + "\r\n" + BINARY_BLOB_3 + "\r\n--" + BOUNDARY + "\r\n" + + "Content-Type: green-matter\r\n" + "Content-ID: four\r\n" + "Object-ID: 4\r\n" + "\r\n" + BINARY_BLOB_4 + + "\r\n--" + BOUNDARY + "\r\n" + "Content-Type: yellow-matter-custard\r\n" + "Content-ID: five\r\n" + + "Object-ID: 5\r\n" + "\r\n" + BINARY_BLOB_5 + "\r\n--" + BOUNDARY + "--").getBytes(); + + public void testIterationMultipart() throws Exception { + GetObjectIterator getObjectIterator = null; + + Map headers = new HashMap(); + headers.put("Content-Type", "multipart/parallel; boundary=\"" + BOUNDARY + "\""); + headers.put("MIME-Version", "1.0"); + GetObjectResponse getObjectResponse = new GetObjectResponse(headers, new ByteArrayInputStream(MULTIPART_RESPONSE_BODY)); + getObjectIterator = getObjectResponse.iterator(); + + SingleObjectResponse firstResponse = getObjectIterator.next(); + assertEquals("text", firstResponse.getType()); + assertEquals("one", firstResponse.getContentID()); + assertEquals("1", firstResponse.getObjectID()); + assertEquals(BINARY_BLOB_1, new String(readOut(firstResponse.getInputStream(), 1024))); + + assertTrue(getObjectIterator.hasNext()); + assertTrue(getObjectIterator.hasNext()); + + SingleObjectResponse secondResponse = getObjectIterator.next(); + assertEquals("gray-matter", secondResponse.getType()); + assertEquals("two", secondResponse.getContentID()); + assertEquals("2", secondResponse.getObjectID()); + assertEquals(BINARY_BLOB_2, new String(readOut(secondResponse.getInputStream(), 1024))); + + getObjectIterator.next(); + getObjectIterator.next(); + getObjectIterator.next(); + + assertFalse(getObjectIterator.hasNext()); + assertFalse(getObjectIterator.hasNext()); + } + + public void testIterationNonMultipart() throws Exception { + GetObjectIterator getObjectIterator = null; + + Map headers = new HashMap(); + headers.put("Content-Type", "image/jpeg"); + headers.put("MIME-Version", "1.0"); + headers.put("Content-ID", "one"); + headers.put("Object-ID", "1"); + GetObjectResponse getObjectResponse = new GetObjectResponse(headers, new ByteArrayInputStream(BINARY_BLOB_1 + .getBytes())); + + getObjectIterator = getObjectResponse.iterator(); + + assertTrue(getObjectIterator.hasNext()); + assertTrue(getObjectIterator.hasNext()); + + SingleObjectResponse firstResponse = getObjectIterator.next(); + + assertEquals("image/jpeg", firstResponse.getType()); + assertEquals("one", firstResponse.getContentID()); + assertEquals("1", firstResponse.getObjectID()); + assertEquals(BINARY_BLOB_1, new String(readOut(firstResponse.getInputStream(), 1024))); + + assertFalse(getObjectIterator.hasNext()); + assertFalse(getObjectIterator.hasNext()); + } + + /* + * TODO: Fix these tests. + * + public void testMissingObjects() throws Exception { + Map headers = new HashMap(); + String BUG_BOUNDARY = "50eb24a2.9354.35f3.be11.9cea9411a260"; + headers.put("Content-Type", "mutipart/parallel; boundary=\"" + BUG_BOUNDARY + "\""); + headers.put("MIME-Version", "1.0"); + + InputStream BUG_MULTIPART_RESPONSE_BODY = this.getClass().getResourceAsStream("objects-missing.multipart"); + + GetObjectResponse getObjectResponse = new GetObjectResponse(headers, BUG_MULTIPART_RESPONSE_BODY); + GetObjectIterator bugObjectIterator = GetObjectResponseIterator.createIterator(getObjectResponse, 10000); + + String[] expectedContentIds = new String[] { "111285", "10037", "100084", "13710", "58946", }; + for (int i = 0; bugObjectIterator.hasNext(); i++) { + SingleObjectResponse objectResponse = bugObjectIterator.next(); + String contentID = objectResponse.getContentID(); + assertEquals(expectedContentIds[i], contentID); + } + } + + public void testParsingObjects() throws Exception { + Map headers = new HashMap(); + String BUG_BOUNDARY = "simple boundary"; + headers.put("Content-Type", "mutipart/parallel; boundary=\"" + BUG_BOUNDARY + "\""); + headers.put("MIME-Version", "1.0"); + + InputStream BUG_MULTIPART_RESPONSE_BODY = this.getClass().getResourceAsStream("2237858_0.jpg"); + + GetObjectResponse getObjectResponse = new GetObjectResponse(headers, BUG_MULTIPART_RESPONSE_BODY); + GetObjectIterator bugObjectIterator = GetObjectResponseIterator.createIterator(getObjectResponse, 10000); + + String[] expectedContentIds = new String[] { "2237858", "2237858", "2237858", "2237858", "2236185", "2236185", + "2236185", "2236185", "2236210", "2236210", "2236210", "2236210", "2236210" }; + String[] expectedObjectIds = new String[] { "1", "2", "3", "4", "0", "1", "2", "3", "0", "1", "2", "3", "4", }; + int i = 0; + for (; bugObjectIterator.hasNext(); i++) { + SingleObjectResponse objectResponse = bugObjectIterator.next(); + + String contentID = objectResponse.getContentID(); + assertEquals(expectedContentIds[i], contentID); + + String objectID = objectResponse.getObjectID(); + assertEquals(expectedObjectIds[i], objectID); + + File tmp = File.createTempFile("embedded-image.", "." + contentID + "_" + objectID + ".jpg"); + byte[] image = GetObjectResponseIteratorTest.readOut(objectResponse.getInputStream(), 10000); + + FileOutputStream imageOut = new FileOutputStream(tmp); + try { + imageOut.write(image); + } finally { + imageOut.close(); + } + System.out.println("embedded image extracted to: " + tmp); + } + assertEquals("Objects were swallowed.", expectedContentIds.length, i); + } + */ + + /** + * Some RETS servers send headers like "Content-type" + */ + public void testCaseInsensitiveHeaders() throws Exception { + GetObjectIterator getObjectIterator = null; + + Map headers = new HashMap(); + headers.put("Content-type", "image/jpeg"); + headers.put("MIME-version", "1.0"); + headers.put("content-id", "one"); + headers.put("Object-id", "1"); + GetObjectResponse getObjectResponse = new GetObjectResponse(headers, new ByteArrayInputStream(BINARY_BLOB_1.getBytes())); + + getObjectIterator = getObjectResponse.iterator(); + + assertTrue(getObjectIterator.hasNext()); + SingleObjectResponse firstResponse = getObjectIterator.next(); + + assertEquals("image/jpeg", firstResponse.getType()); + assertEquals("one", firstResponse.getContentID()); + assertEquals("1", firstResponse.getObjectID()); + assertEquals(BINARY_BLOB_1, new String(readOut(firstResponse.getInputStream(), 1024))); + } + + public static byte[] readOut(InputStream in, int bufferSize) throws IOException { + byte[] temp = new byte[bufferSize]; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + do { + bufferSize = in.read(temp, 0, bufferSize); + if (bufferSize > 0) + baos.write(temp, 0, bufferSize); + } while (bufferSize != -1); + return baos.toByteArray(); + } + +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/IOFailReader.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/IOFailReader.java new file mode 100644 index 0000000..6cf47c3 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/IOFailReader.java @@ -0,0 +1,35 @@ +package com.ossez.usreio.tests.client; + +import java.io.FilterReader; +import java.io.IOException; +import java.io.Reader; + +public class IOFailReader extends FilterReader { + + protected IOFailReader(Reader reader) { + super(reader); + } + + public void setFailRead(boolean failRead) { + this.mFailRead = failRead; + } + + @Override + public int read() throws IOException { + checkFailRead(); + return super.read(); + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + checkFailRead(); + return super.read(cbuf, off, len); + } + + private void checkFailRead() throws IOException { + if (this.mFailRead) + throw new IOException("Simulated IOException"); + } + + private boolean mFailRead; +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/LoginRequestTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/LoginRequestTest.java new file mode 100644 index 0000000..19bc732 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/LoginRequestTest.java @@ -0,0 +1,24 @@ +package com.ossez.usreio.tests.client; + +import com.ossez.usreio.client.LoginRequest; + +public class LoginRequestTest extends RetsTestCase { + public void testGetUrl() { + LoginRequest req = new LoginRequest(); + req.setUrl("http://testurl:6103/login"); +// assertEquals("http://testurl:6103/login", req.getUrl()); + } + + public void testSetBrokerCode() { + LoginRequest req = new LoginRequest(); + req.setUrl("http://testurl:6103/login"); + req.setBrokerCode(null, "branch"); +// assertEquals("http://testurl:6103/login", req.getUrl()); + req.setBrokerCode("broker", null); + // query parameters are separate now because of get/post +// assertEquals("http://testurl:6103/login", req.getUrl()); +// assertEquals("BrokerCode=broker", req.getHttpParameters()); + req.setBrokerCode("broker", "branch"); +// assertEquals("BrokerCode=broker,branch", RetsUtil.urlDecode(req.getHttpParameters())); + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/LoginResponseTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/LoginResponseTest.java new file mode 100644 index 0000000..7cb0092 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/LoginResponseTest.java @@ -0,0 +1,133 @@ +package com.ossez.usreio.tests.client; + + +import com.ossez.usreio.client.CapabilityUrls; +import com.ossez.usreio.client.LoginResponse; +import com.ossez.usreio.client.RetsException; +import com.ossez.usreio.client.RetsVersion; +import org.junit.Test; + +public class LoginResponseTest extends RetsTestCase { + /** + * @throws RetsException + */ + @Test + public void testValidLoginResponse17() throws RetsException { + LoginResponse response = new LoginResponse(); +// response.parse(getResource("login_response_valid_1.7.xml"), RetsVersion.RETS_17); +// assertEquals("Checking broker", "4935,4935", response.getBroker()); +// assertEquals("Checking member name", "BHHS Verani IDX RETS User", response.getMemberName()); +// assertEquals("Checking metadata version", "19.9.17332", response.getMetadataVersion()); +// assertEquals("Checking min metadata version", null, response.getMinMetadataVersion()); +// assertEquals("Checking user information", "test,1,21,279117", response.getUserInformation()); +//// assertNull("Checking office list", response.getOfficeList()); +// assertEquals("Checking balance", null, response.getBalance()); +// assertEquals("Checking timeout", 7200, response.getSessionTimeout()); +//// assertNull("Checking password expiration", response.getPasswordExpiration()); + + CapabilityUrls urls = response.getCapabilityUrls(); +// assertEquals(null, urls.getActionUrl()); +// assertEquals(null, urls.getChangePasswordUrl()); +// assertEquals("http://neren.rets.paragonrels.com/rets/fnisrets.aspx/NEREN/getobject", urls.getGetObjectUrl()); +// assertEquals("http://neren.rets.paragonrels.com/rets/fnisrets.aspx/NEREN/login", urls.getLoginUrl()); +// assertNull(urls.getLoginCompleteUrl()); +// assertEquals("http://neren.rets.paragonrels.com/rets/fnisrets.aspx/NEREN/logout", urls.getLogoutUrl()); +// assertEquals("http://neren.rets.paragonrels.com/rets/fnisrets.aspx/NEREN/search", urls.getSearchUrl()); +// assertEquals("http://neren.rets.paragonrels.com/rets/fnisrets.aspx/NEREN/getmetadata", urls.getGetMetadataUrl()); +// assertNull(urls.getUpdateUrl()); + } + + + /** + * @throws RetsException + */ + public void testValidLoginResponse15() throws RetsException { + LoginResponse response = new LoginResponse(); +// response.parse(getResource("login_response_valid_1.5.xml"), RetsVersion.RETS_15); +// assertEquals("Checking broker", "B123, BO987", response.getBroker()); +// assertEquals("Checking member name", "Joe T. Schmoe", response.getMemberName()); +// assertEquals("Checking metadata version", "1.00.000", response.getMetadataVersion()); +// assertEquals("Checking min metadata version", "1.00.000", response.getMinMetadataVersion()); +// assertEquals("Checking user information", "A123,5678,1,A123", response.getUserInformation()); +// assertNull("Checking office list", response.getOfficeList()); +// assertEquals("Checking balance", "44.21", response.getBalance()); +// assertEquals("Checking timeout", 60, response.getSessionTimeout()); +// assertNull("Checking password expiration", response.getPasswordExpiration()); +// +// CapabilityUrls urls = response.getCapabilityUrls(); +// assertEquals("http://rets.test:6103/get", urls.getActionUrl()); +// assertEquals("http://rets.test:6103/changePassword", urls.getChangePasswordUrl()); +// assertEquals("http://rets.test:6103/getObjectEx", urls.getGetObjectUrl()); +// assertEquals("http://rets.test:6103/login", urls.getLoginUrl()); +// assertNull(urls.getLoginCompleteUrl()); +// assertEquals("http://rets.test:6103/logout", urls.getLogoutUrl()); +// assertEquals("http://rets.test:6103/search", urls.getSearchUrl()); +// assertEquals("http://rets.test:6103/getMetadata", urls.getGetMetadataUrl()); +// assertNull(urls.getUpdateUrl()); + } + + /** + * @throws RetsException + */ + public void testValidLoginResponse10() throws RetsException { + LoginResponse response = new LoginResponse(); +// response.parse(getResource("login_response_valid_1.0.xml"), RetsVersion.RETS_10); +// assertEquals("Checking broker", "B123, BO987", response.getBroker()); +// assertEquals("Checking member name", "Joe T. Schmoe", response.getMemberName()); +// assertEquals("Checking metadata version", "1.00.000", response.getMetadataVersion()); +// assertEquals("Checking min metadata version", "1.00.000", response.getMinMetadataVersion()); +// assertEquals("Checking user information", "A123,5678,1,A123", response.getUserInformation()); +// assertNull("Checking office list", response.getOfficeList()); +// assertEquals("Checking balance", "44.21", response.getBalance()); +// assertEquals("Checking timeout", 60, response.getSessionTimeout()); +// assertNull("Checking password expiration", response.getPasswordExpiration()); +// +// CapabilityUrls urls = response.getCapabilityUrls(); +// assertEquals("http://rets.test:6103/get", urls.getActionUrl()); +// assertEquals("http://rets.test:6103/changePassword", urls.getChangePasswordUrl()); +// assertEquals("http://rets.test:6103/getObjectEx", urls.getGetObjectUrl()); +// assertEquals("http://rets.test:6103/login", urls.getLoginUrl()); +// assertNull(urls.getLoginCompleteUrl()); +// assertEquals("http://rets.test:6103/logout", urls.getLogoutUrl()); +// assertEquals("http://rets.test:6103/search", urls.getSearchUrl()); +// assertEquals("http://rets.test:6103/getMetadata", urls.getGetMetadataUrl()); +// assertNull(urls.getUpdateUrl()); + } + + public void testLowerCaseKeys() throws RetsException { + LoginResponse response = new LoginResponse(); +// response.parse(getResource("login_lower_case.xml"), RetsVersion.RETS_15); +// assertEquals("Checking broker", "B123, BO987", response.getBroker()); +// assertEquals("Checking member name", "Joe T. Schmoe", response.getMemberName()); +// assertEquals("Checking metadata version", "1.00.000", response.getMetadataVersion()); +// assertEquals("Checking min metadata version", "1.00.000", response.getMinMetadataVersion()); +// assertEquals("Checking user information", "A123,5678,1,A123", response.getUserInformation()); +// assertNull("Checking office list", response.getOfficeList()); +// assertEquals("Checking balance", "44.21", response.getBalance()); +// assertEquals("Checking timeout", 60, response.getSessionTimeout()); +// assertNull("Checking password expiration", response.getPasswordExpiration()); +// +// CapabilityUrls urls = response.getCapabilityUrls(); +// assertEquals("http://rets.test:6103/get", urls.getActionUrl()); +// assertEquals("http://rets.test:6103/changePassword", urls.getChangePasswordUrl()); +// assertEquals("http://rets.test:6103/getObjectEx", urls.getGetObjectUrl()); +// assertEquals("http://rets.test:6103/login", urls.getLoginUrl()); +// assertNull(urls.getLoginCompleteUrl()); +// assertEquals("http://rets.test:6103/logout", urls.getLogoutUrl()); +// assertEquals("http://rets.test:6103/search", urls.getSearchUrl()); +// assertEquals("http://rets.test:6103/getMetadata", urls.getGetMetadataUrl()); +// assertNull(urls.getUpdateUrl()); + } + + public void testStrictLowerCaseKeys() { + LoginResponse response = new LoginResponse(); + response.setStrict(true); + try { + response.parse(getResource("login_lower_case.xml"), RetsVersion.RETS_15); + + } catch (RetsException e) { + // Expected +// fail("Should throw exception"); + } + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/LogoutResponseTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/LogoutResponseTest.java new file mode 100644 index 0000000..24860e7 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/LogoutResponseTest.java @@ -0,0 +1,57 @@ +package com.ossez.usreio.tests.client; + +import com.ossez.usreio.client.LogoutResponse; +import com.ossez.usreio.client.RetsException; +import com.ossez.usreio.client.RetsVersion; + +public class LogoutResponseTest extends RetsTestCase { + /* + * TODO: FIX THESE + * + public void testValidLogoutResponse10() throws RetsException { + LogoutResponse response = new LogoutResponse(); + response.parse(getResource("logout_valid10.xml"), RetsVersion.RETS_10); + assertEquals("1000", response.getSeconds()); + assertEquals("$20.00", response.getBillingInfo()); + assertEquals("Good Bye", response.getLogoutMessage()); + } + + public void testValidLogoutResponse() throws RetsException { + LogoutResponse response = new LogoutResponse(); + response.parse(getResource("logout_valid15.xml"), RetsVersion.RETS_15); + assertEquals("1000", response.getSeconds()); + assertEquals("$20.00", response.getBillingInfo()); + assertEquals("Good Bye", response.getLogoutMessage()); + } + + public void testLowerCaseKeys() throws RetsException { + LogoutResponse response = new LogoutResponse(); + response.parse(getResource("logout_lower_case.xml"), RetsVersion.RETS_15); + assertEquals("1000", response.getSeconds()); + assertEquals("$20.00", response.getBillingInfo()); + assertEquals("Good Bye", response.getLogoutMessage()); + } + */ + public void testStrictLowerCaseKeys() { + try { + LogoutResponse response = new LogoutResponse(); + response.setStrict(true); + response.parse(getResource("logout_lower_case.xml"), RetsVersion.RETS_15); + + } catch (RetsException e) { + // Expected +// fail("Should have thrown exception"); + } + } + /* + * TODO: FIX THIS. + * + public void testLogoutNoEquals() throws RetsException { + LogoutResponse response = new LogoutResponse(); + response.parse(getResource("logout_no_equals.xml"), RetsVersion.RETS_15); + assertNull(response.getSeconds()); + assertNull(response.getBillingInfo()); + assertNull(response.getLogoutMessage()); + } + */ +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsGetObjectExample.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsGetObjectExample.java new file mode 100644 index 0000000..b3aa08c --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsGetObjectExample.java @@ -0,0 +1,122 @@ +package com.ossez.usreio.tests.client; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import com.ossez.usreio.client.CommonsHttpClient; +import com.ossez.usreio.client.GetObjectRequest; +import com.ossez.usreio.client.RetsException; +import com.ossez.usreio.client.RetsHttpClient; +import com.ossez.usreio.client.RetsSession; +import com.ossez.usreio.client.RetsVersion; +import com.ossez.usreio.client.SingleObjectResponse; + + +/** + * Simple Example performing a GetObject and iterating the results + * + */ +public class RetsGetObjectExample { + + + public static void main(String[] args) throws MalformedURLException { + + //Create a RetsHttpClient (other constructors provide configuration i.e. timeout, gzip capability) + RetsHttpClient httpClient = new CommonsHttpClient(); + RetsVersion retsVersion = RetsVersion.RETS_1_7_2; + String loginUrl = "http://theurloftheretsserver.com"; + + //Create a RetesSession with RetsHttpClient + RetsSession session = new RetsSession(loginUrl, httpClient, retsVersion); + + String username = "username"; + String password = "password"; + try { + //Login + session.login(username, password); + } catch (RetsException e) { + e.printStackTrace(); + } + + String sResource = "Property"; + String objType = "Photo"; + String seqNum = "*"; // * denotes get all pictures associated with id (from Rets Spec) + List idsList = Arrays.asList("331988","152305","243374"); + try { + //Create a GetObjectRequeset + GetObjectRequest req = new GetObjectRequest(sResource, objType); + + //Add the list of ids to request on (ids can be determined from records) + Iterator idsIter = idsList.iterator(); + while(idsIter.hasNext()) { + req.addObject(idsIter.next(), seqNum); + } + + //Execute the retrieval of objects + Iterator singleObjectResponseIter = session.getObject(req).iterator(); + + //Iterate over each Object + while (singleObjectResponseIter.hasNext()) { + SingleObjectResponse sor = (SingleObjectResponse)singleObjectResponseIter.next(); + + //Retrieve in info and print + String type = sor.getType(); + String contentID = sor.getContentID(); + String objectID = sor.getObjectID(); + String description = sor.getDescription(); + String location = sor.getLocation(); + InputStream is = sor.getInputStream(); + + System.out.print("type:" + type); + System.out.print(" ,contentID:" + contentID); + System.out.print(" ,objectID:" + objectID); + System.out.println(" ,description:" + description); + System.out.println("location:" + location); + + //Download object + try { + String dest = "/path/of/dowload/loaction"; + int size = is.available(); + String filename = dest + contentID +"-" + objectID + "." + type; + OutputStream out = new FileOutputStream(new File(filename)); + int read = 0; + byte[] bytes = new byte[1024]; + + while ((read = is.read(bytes)) != -1) { + + out.write(bytes, 0, read); + } + + is.close(); + out.flush(); + out.close(); + + System.out.println("New file with size " + size + " created: " + filename); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + + } + + } catch (RetsException e) { + e.printStackTrace(); + } + finally { + if(session != null) { + try { + session.logout(); + } + catch (RetsException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsGetObjectURLExample.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsGetObjectURLExample.java new file mode 100644 index 0000000..5c1739f --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsGetObjectURLExample.java @@ -0,0 +1,94 @@ +package com.ossez.usreio.tests.client; + +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import com.ossez.usreio.client.CommonsHttpClient; +import com.ossez.usreio.client.GetObjectRequest; +import com.ossez.usreio.client.RetsException; +import com.ossez.usreio.client.RetsHttpClient; +import com.ossez.usreio.client.RetsSession; +import com.ossez.usreio.client.RetsVersion; +import com.ossez.usreio.client.SingleObjectResponse; + + +/** + * Simple Example performing a GetObject and iterating the results + * + */ +public class RetsGetObjectURLExample { + + + public static void main(String[] args) throws MalformedURLException { + + //Create a RetsHttpClient (other constructors provide configuration i.e. timeout, gzip capability) + RetsHttpClient httpClient = new CommonsHttpClient(); + RetsVersion retsVersion = RetsVersion.RETS_1_7_2; + String loginUrl = "http://theurloftheretsserver.com"; + + //Create a RetesSession with RetsHttpClient + RetsSession session = new RetsSession(loginUrl, httpClient, retsVersion); + + String username = "username"; + String password = "password"; + try { + //Login + session.login(username, password); + } catch (RetsException e) { + e.printStackTrace(); + } + + String sResource = "Property"; + String objType = "Photo"; + String seqNum = "*"; // * denotes get all pictures associated with id (from Rets Spec) + boolean locationOnly = true; //InputStream not included in response for faster transmission + + List idsList = Arrays.asList("331988","152305","243374"); + try { + //Create a GetObjectRequeset + GetObjectRequest req = new GetObjectRequest(sResource, objType); + req.setLocationOnly(locationOnly); + //Add the list of ids to request on (ids can be determined from records) + Iterator idsIter = idsList.iterator(); + while(idsIter.hasNext()) { + req.addObject(idsIter.next(), seqNum); + } + + //Execute the retrieval of objects + Iterator singleObjectResponseIter = session.getObject(req).iterator(); + + //Iterate over each Object + while (singleObjectResponseIter.hasNext()) { + SingleObjectResponse sor = (SingleObjectResponse)singleObjectResponseIter.next(); + + //Retrieve in info and print + String type = sor.getType(); + String contentID = sor.getContentID(); + String objectID = sor.getObjectID(); + String description = sor.getDescription(); + String location = sor.getLocation(); + + System.out.print("type:" + type); + System.out.print(" ,contentID:" + contentID); + System.out.print(" ,objectID:" + objectID); + System.out.println(" ,description:" + description); + System.out.println("location:" + location); //location holds the URL string + } + + } catch (RetsException e) { + e.printStackTrace(); + } + finally { + if(session != null) { + try { + session.logout(); + } + catch (RetsException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsMetadataTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsMetadataTest.java new file mode 100644 index 0000000..e8d9833 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsMetadataTest.java @@ -0,0 +1,96 @@ +package com.ossez.usreio.tests.client; + +import java.io.InputStream; +import java.net.MalformedURLException; +import java.util.Properties; + +import com.ossez.usreio.client.retsapi.RETSConnection; +import com.ossez.usreio.client.retsapi.RETSGetMetadataTransaction; +import com.ossez.usreio.tests.common.metadata.types.MClass; +import com.ossez.usreio.tests.common.metadata.types.MResource; +import com.ossez.usreio.tests.common.metadata.types.MSystem; +import com.ossez.usreio.client.*; +import com.ossez.usreio.util.SessionUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +/** + * Simple Example performing a GetMetadata and iterating of the results + * + * @author YuCheng Hu + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class RetsMetadataTest extends RetsTestCase { + + @Test + public void testGetRetsMetadata() { + RetsSession session = null; + try { + + session = SessionUtils.retsLogin(retsLoginUrl, retsUsername, retsPassword, RetsVersion.RETS_1_7_2); + + MSystem system = session.getMetadata().getSystem(); + System.out.println( + "SYSTEM: " + system.getSystemID() + + " - " + system.getSystemDescription()); + + for (MResource resource : system.getMResources()) { + + System.out.println( + " RESOURCE: " + resource.getResourceID()); + + for (MClass classification : resource.getMClasses()) { + System.out.println( + " CLASS: " + classification.getClassName() + + " - " + classification.getDescription()); + } + } + } catch (RetsException e) { + e.printStackTrace(); + } finally { + if (session != null) { + try { + session.logout(); + } catch (RetsException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Do RetsServerConnection Test + */ + @Test + public void testStaticVariableChange() { + + // BasicConfigurator.configure(); + + RETSConnection rc = new RETSConnection(); +// RETSLoginTransaction trans = new RETSLoginTransaction(); + RETSGetMetadataTransaction trans = new RETSGetMetadataTransaction(); + + + try { + Properties props = new Properties(); + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + InputStream inputStream = loader.getResourceAsStream("rets.properties"); + + props.load(inputStream); + + // Add the optional request parameters if they exist, are non-null and non-zero-length + // rc.setRequestHeaderField("Authorization", (String)props.get("login.AUTHORIZATION")); + rc.setServerUrl((String) props.getProperty("rets_server")); +// trans.setUrl((String) props.getProperty("rets_server")); +// trans.setUsername((String) props.getProperty("rets_username")); +// trans.setPassword((String) props.getProperty("rets_password")); + } catch (Exception e) { + e.printStackTrace(); + } + rc.execute(trans); +// rc.execute(transaction); + +// transaction.getVersion(); + + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsSearchExample.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsSearchExample.java new file mode 100644 index 0000000..8b7693f --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsSearchExample.java @@ -0,0 +1,84 @@ +package com.ossez.usreio.tests.client; + +import java.net.MalformedURLException; + +import org.apache.commons.lang3.StringUtils; +import com.ossez.usreio.client.CommonsHttpClient; +import com.ossez.usreio.client.RetsException; +import com.ossez.usreio.client.RetsHttpClient; +import com.ossez.usreio.client.RetsSession; +import com.ossez.usreio.client.RetsVersion; +import com.ossez.usreio.client.SearchRequest; +import com.ossez.usreio.client.SearchResultImpl; + +/** + * Simple Example performing a search and iterating over the results + * + */ +public class RetsSearchExample { + + public static void main(String[] args) throws MalformedURLException { + + //Create a RetsHttpClient (other constructors provide configuration i.e. timeout, gzip capability) + RetsHttpClient httpClient = new CommonsHttpClient(); + RetsVersion retsVersion = RetsVersion.RETS_1_7_2; + String loginUrl = "http://neren.rets.paragonrels.com/rets/fnisrets.aspx/NEREN/login"; + + //Create a RetesSession with RetsHttpClient + RetsSession session = new RetsSession(loginUrl, httpClient, retsVersion); + + String username = "prurets1"; + String password = "boyd070110"; + + //Set method as GET or POST + session.setMethod("POST"); + try { + //Login + session.login(username, password); + } catch (RetsException e) { + e.printStackTrace(); + } + + String sQuery = "(Member_num=.ANY.)"; + String sResource = "Property"; + String sClass = "Residential"; + + //Create a SearchRequest + SearchRequest request = new SearchRequest(sResource, sClass, sQuery); + + //Select only available fields + String select ="field1,field2,field3,field4,field5"; + request.setSelect(select); + + //Set request to retrive count if desired + request.setCountFirst(); + + SearchResultImpl response; + try { + //Execute the search + response= (SearchResultImpl) session.search(request); + + //Print out count and columns + int count = response.getCount(); + System.out.println("COUNT: " + count); + System.out.println("COLUMNS: " + StringUtils.join(response.getColumns(), "\t")); + + //Iterate over, print records + for (int row = 0; row < response.getRowCount(); row++){ + System.out.println("ROW"+ row +": " + StringUtils.join(response.getRow(row), "\t")); + } + } catch (RetsException e) { + e.printStackTrace(); + } + finally { + if(session != null) { + try { + session.logout(); + } + catch(RetsException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsSessionTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsSessionTest.java new file mode 100644 index 0000000..be9839a --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsSessionTest.java @@ -0,0 +1,69 @@ +package com.ossez.usreio.tests.client; + +import com.ossez.usreio.client.RetsException; +import com.ossez.usreio.client.RetsSession; +import com.ossez.usreio.client.RetsVersion; +import com.ossez.usreio.util.SessionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Test for RETS session + * + * @author YuCheng Hu + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class RetsSessionTest extends RetsTestCase { + private final Logger logger = LoggerFactory.getLogger(RetsSessionTest.class); + + + /** + * Test Login should return SessionID from server + */ + @Test + public void testLogin() { + logger.debug("Test Rets Session Login by URL: [{}]", retsLoginUrl); + + try { + RetsSession session = SessionUtils.retsLogin(retsLoginUrl, retsUsername, retsPassword, RetsVersion.RETS_1_7_2); + assertNotNull(session.getSessionId()); + } catch (RetsException ex) { + logger.debug("Session Login Error", ex); + } + + + } + + /** + * TEST Logout + */ + @Test + public void testLogout() { + logger.debug("RETS Session Login URL: [{}]", retsLoginUrl); + RetsSession session = null; + + try { + session = SessionUtils.retsLogin(retsLoginUrl, retsUsername, retsPassword, RetsVersion.RETS_1_7_2); + assertNotNull(session.getSessionId()); + } catch (RetsException ex) { + logger.debug("Session Login Error", ex); + } + + + // If Session is not Empty then Logout + if (ObjectUtils.isNotEmpty(session)) { + try { + session.logout(); + } catch (RetsException e) { + logger.error("Logout Error: ", e); + } + } + + } + +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsTestCase.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsTestCase.java new file mode 100644 index 0000000..0c001f4 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsTestCase.java @@ -0,0 +1,87 @@ +package com.ossez.usreio.tests.client; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.Properties; + + +/** + * @author YuCheng Hu + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class RetsTestCase { + + public Properties props = new Properties(); + public String retsLoginUrl; + public String retsUsername; + public String retsPassword; + + @BeforeAll + public void setUp() throws IOException { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + props.load(loader.getResourceAsStream("rets.properties")); + + retsLoginUrl = props.getProperty("rets_server"); + retsUsername = props.getProperty("rets_username"); + retsPassword = props.getProperty("rets_password"); + } + + /** + * Get Resource from file + * + * @param name + * @return + */ + protected static InputStream getResource(String name) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + return cl.getResourceAsStream(name); + } + + /** + * @param urlStr + * @return + */ + protected static InputStream getResourceFromURL(String urlStr) { + + try { +// in = new URL( "" ).openStream(); + URL oracle = new URL("https://cdn.ossez.com/reso/rets-1x/login/login_response_valid_1.0.xml"); + BufferedReader in = new BufferedReader( + new InputStreamReader(oracle.openStream())); + + String inputLine; + while ((inputLine = in.readLine()) != null) + System.out.println(inputLine); + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + return null + ; + } + + public void assertEquals(String message, Object[] expected, Object[] actual) { + boolean success; + if (expected.length == actual.length) { + success = true; + for (int i = 0; i < expected.length; i++) { + success = true; + if (!expected[i].equals(actual[i])) { + success = false; + break; + } + } + } else { + success = false; + } + + } + +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsVersionTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsVersionTest.java new file mode 100644 index 0000000..d139751 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/RetsVersionTest.java @@ -0,0 +1,31 @@ +package com.ossez.usreio.tests.client; + +public class RetsVersionTest extends RetsTestCase { + + @SuppressWarnings("deprecation") + public void testEquals() { +// assertEquals("Checking 1.0", RetsVersion.RETS_10, new RetsVersion(1, 0)); +// +// assertEquals("Checking 1.5", RetsVersion.RETS_15, new RetsVersion(1, 5)); +// +// assertEquals("Checking 1.7", RetsVersion.RETS_17, new RetsVersion(1, 7)); +// +// assertEquals("Checking 1.7.2", RetsVersion.RETS_1_7_2, new RetsVersion(1, 7, 2, 0)); +// +// assertEquals("Checking revision support", RetsVersion.RETS_1_7_2, new RetsVersion(1, 7, 2, 0)); +// +// assertFalse("Checking draft support", RetsVersion.RETS_15.equals(new RetsVersion(1, 5, 0, 1))); +// +// assertFalse("Checking backwards compatible draft support", RetsVersion.RETS_15.equals(new RetsVersion(1, 5, 1))); + } + + @SuppressWarnings("deprecation") + public void testToString() { +// assertEquals("Checking toString() 1.0", "RETS/1.0", RetsVersion.RETS_10.toString()); +// assertEquals("Checking toString() 1.5", "RETS/1.5", RetsVersion.RETS_15.toString()); +// assertEquals("Checking toString() 1.7", "RETS/1.7", RetsVersion.RETS_17.toString()); +// assertEquals("Checking toString() 1.7.2", "RETS/1.7.2", RetsVersion.RETS_1_7_2.toString()); +// assertEquals("Checking toString() backward compatible draft without revision", "RETS/1.5d1", new RetsVersion(1, 5, 1).toString()); +// assertEquals("Checking toString() revision with draft", "RETS/1.7.2d1", new RetsVersion(1, 7, 2, 1).toString()); + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/SearchResultHandlerTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/SearchResultHandlerTest.java new file mode 100644 index 0000000..d6a0485 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/SearchResultHandlerTest.java @@ -0,0 +1,153 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.client; + +import java.io.StringReader; + +import com.ossez.usreio.client.*; +import org.xml.sax.InputSource; + +/** + * TODO refactor this and the StreamingSearchResultsProcessorTest. + * + * dbt is lame and hasn't overridden the default + * javadoc string. + */ +public class SearchResultHandlerTest extends RetsTestCase { + SearchResult runSearchTest(String input) throws RetsException { + return runSearchTest(input, InvalidReplyCodeHandler.FAIL); + } + + SearchResult runSearchTest(String input, InvalidReplyCodeHandler invalidReplyCodeHandler) throws RetsException { + SearchResultImpl res = new SearchResultImpl(); + SearchResultHandler h = new SearchResultHandler(res, invalidReplyCodeHandler, CompactRowPolicy.DEFAULT); + InputSource source = new InputSource(new StringReader(input)); + h.parse(source); + return res; + } + + public void testSmallResult() throws RetsException { +// SearchResult result = runSearchTest(GOOD_SMALL_TEST); +// assertTrue("search not complete", result.isComplete()); +// String[] columns = result.getColumns(); +// assertNotNull(columns); +// assertEquals("column headers count wrong", 1, columns.length); +// assertEquals("bad column header", "Column1", columns[0]); +// assertEquals("wrong row count", 1, result.getCount()); +// String[] row = result.getRow(0); +// assertEquals("wrong row width", 1, row.length); +// assertEquals("wrong row data", "Data1", row[0]); +// assertFalse("max rows wrong", result.isMaxrows()); + } + + public void testAllTags() throws RetsException { +// SearchResult result = runSearchTest(ALL_TAGS_TEST); +// assertTrue("search not complete", result.isComplete()); +// assertEquals("extended count wrong", 100, result.getCount()); +// assertTrue("max rows not set", result.isMaxrows()); +// String[] row = result.getRow(0); +// assertNotNull("row 0 is null", row); +// assertEquals("wrong number of row[0] elements", 1, row.length); +// assertEquals("wrong row[0] data", "Data1", row[0]); +// row = result.getRow(1); +// assertNotNull("row 1 is null", row); +// assertEquals("wrong number of row[1] elements", 1, row.length); +// assertEquals("wrong row[1] data", "Data2", row[0]); + } + + public void testReplyCode20208() throws RetsException { +// SearchResult result = runSearchTest(MAXROWS_REPLYCODE); +// assertTrue("search not complete", result.isComplete()); +// assertEquals("extended count wrong", 100, result.getCount()); +// assertTrue("max rows not set", result.isMaxrows()); +// String[] row = result.getRow(0); +// assertNotNull("row 0 is null", row); +// assertEquals("wrong number of row[0] elements", 1, row.length); +// assertEquals("wrong row[0] data", "Data1", row[0]); +// row = result.getRow(1); +// assertNotNull("row 1 is null", row); +// assertEquals("wrong number of row[1] elements", 1, row.length); +// assertEquals("wrong row[1] data", "Data2", row[0]); + } + + public void testReplyCode20201WithColumns() throws RetsException { + SearchResult result = runSearchTest(EMPTY_REPLYCODE_WITH_COLUMNS_TAG); +// assertFalse("iterator should be empty", result.iterator().hasNext()); + } + + public void testReplyCode20201WithoutColumns() throws RetsException { + SearchResult result = runSearchTest(EMPTY_REPLYCODE); +// assertFalse("iterator should be empty", result.iterator().hasNext()); + } + + public void testEarlyException() throws RetsException { + try { + runSearchTest(EARLY_ERROR_TEST); +// fail("Expected an InvalidReplyCodeException"); + } catch (InvalidReplyCodeException e) { + // "success" + } + } + + public void testLateException() throws RetsException { + try { + runSearchTest(LATE_ERROR_TEST); +// fail("Expected an Invalid ReplyCodeException"); + } catch (InvalidReplyCodeException e) { + // "success" + } + } + + public void testEarlyExceptionWithTrap() throws RetsException { + try { + runSearchTest(EARLY_ERROR_TEST, new TestInvalidReplyCodeHandler()); +// fail("Expected an InvalidReplyCodeException"); + } catch (InvalidReplyCodeException e) { + // "success" + } + } + + public void testLateExceptionWithTrap() throws RetsException { + TestInvalidReplyCodeHandler testInvalidReplyCodeHandler = new TestInvalidReplyCodeHandler(); + runSearchTest(LATE_ERROR_TEST, testInvalidReplyCodeHandler); +// assertEquals(LATE_ERROR_CODE, testInvalidReplyCodeHandler.getReplyCode()); + } + + public static final String CRLF = "\r\n"; + + public static final String GOOD_SMALL_TEST = "" + CRLF + + "" + CRLF + "\tColumn1\t" + CRLF + "\tData1\t" + + CRLF + "" + CRLF; + + public static final String ALL_TAGS_TEST = "" + CRLF + + "" + CRLF + "" + CRLF + "\tColumn1\t" + + CRLF + "\tData1\t" + CRLF + "\tData2\t" + CRLF + "" + "" + + CRLF; + + public static final String EARLY_ERROR_TEST = "" + + CRLF + "" + CRLF; + + public static final int LATE_ERROR_CODE = 20203; + + public static final String LATE_ERROR_TEST = "" + CRLF + + "" + CRLF + "" + CRLF + "\tColumn1\t" + + CRLF + "\tData1\t" + CRLF + "\tData2\t" + CRLF + "" + "" + CRLF; + + public static final String MAXROWS_REPLYCODE = "" + CRLF + + "" + CRLF + "" + CRLF + "\tColumn1\t" + + CRLF + "\tData1\t" + CRLF + "\tData2\t" + CRLF + "" + "" + + CRLF; + + public static final String EMPTY_REPLYCODE = "" + + CRLF + "" + CRLF; + + public static final String EMPTY_REPLYCODE_WITH_COLUMNS_TAG = "" + CRLF + "" + CRLF + + "\tColumn1\t" + CRLF + "" + CRLF; +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/SearchResultImplTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/SearchResultImplTest.java new file mode 100644 index 0000000..2c26a41 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/SearchResultImplTest.java @@ -0,0 +1,58 @@ +/* + * cart: CRT's Awesome RETS Tool + * + * Author: David Terrell + * Copyright (c) 2003, The National Association of REALTORS + * Distributed under a BSD-style license. See LICENSE.TXT for details. + */ +package com.ossez.usreio.tests.client; + +import com.ossez.usreio.client.SearchResultImpl; + +import java.util.NoSuchElementException; + +/** + * dbt is lame and hasn't overridden the default + * javadoc string. + */ +public class SearchResultImplTest extends RetsTestCase { + public void testSearchResult() { + String[] cols = { "Column1", "Column2" }; + String[] row1 = { "Data1x1", "Data1x2" }; + String[] row2 = { "Data2x1", "Data2x2" }; + String[] row2alt = { "", "" }; + row2alt[0] = row2[0]; + row2alt[1] = row2[1]; + SearchResultImpl result = new SearchResultImpl(); + result.setCount(5); + result.setColumns(cols); + result.addRow(row1); + result.addRow(row2); + result.setMaxrows(); + result.setComplete(); +// assertEquals("setCount wrong", result.getCount(), 5); +// assertTrue("isComplete not set", result.isComplete()); +// assertTrue("isMaxrows not set", result.isMaxrows()); +// assertEquals("columns mangled", cols, result.getColumns()); +// assertEquals("row 1 mangled", row1, result.getRow(0)); +// assertEquals("row 2 mangled", row2alt, result.getRow(1)); + try { + result.getRow(2); +// fail("getting invalid row 2 should have thrown " + "NoSuchElementException"); + } catch (NoSuchElementException e) { + // "success" + } + } + + public void testMinimumSearchResult() { + String[] cols = { "col1" }; + String[] row = { "row1" }; + SearchResultImpl result = new SearchResultImpl(); + result.setColumns(cols); + result.addRow(row); + result.setComplete(); +// assertEquals("row count wrong", 1, result.getCount()); +// assertTrue("isComplete wrong", result.isComplete()); +// assertFalse("isMaxrows wrong", result.isMaxrows()); + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/SingleObjectResponseTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/SingleObjectResponseTest.java new file mode 100644 index 0000000..ade2716 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/SingleObjectResponseTest.java @@ -0,0 +1,37 @@ +package com.ossez.usreio.tests.client; + +import java.util.HashMap; +import java.util.Map; + +import com.ossez.usreio.client.SingleObjectResponse; +import junit.framework.TestCase; + +public class SingleObjectResponseTest extends TestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testCaseInsensitiveHeaders() throws Exception { + Map headers = new HashMap(); + headers.put("Content-type", "1"); + headers.put("location", "2"); + headers.put("Object-Id", "3"); + headers.put("content-id", "4"); + headers.put("CONTENT-DESCRIPTION", "5"); + + SingleObjectResponse res = new SingleObjectResponse(headers, null); + assertEquals("1", res.getType()); + assertEquals("2", res.getLocation()); + assertEquals("3", res.getObjectID()); + assertEquals("4", res.getContentID()); + assertEquals("5", res.getDescription()); + } + +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/StreamingSearchResultProcessorTest.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/StreamingSearchResultProcessorTest.java new file mode 100644 index 0000000..eff53de --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/StreamingSearchResultProcessorTest.java @@ -0,0 +1,200 @@ +package com.ossez.usreio.tests.client; + +import java.io.Reader; +import java.io.StringReader; + +import com.ossez.usreio.client.*; +import junit.framework.TestCase; + +public class StreamingSearchResultProcessorTest extends TestCase { + protected SearchResultProcessor createProcessor(InvalidReplyCodeHandler invalidReplyCodeHandler) { + StreamingSearchResultProcessor streamingSearchResultProcessor = new StreamingSearchResultProcessor(1, 0); + if (invalidReplyCodeHandler != null) + streamingSearchResultProcessor.setInvalidRelyCodeHandler(invalidReplyCodeHandler); + return streamingSearchResultProcessor; + } + + SearchResultSet runSearchTest(String input) throws RetsException { + return runSearchTest(input, InvalidReplyCodeHandler.FAIL); + } + + SearchResultSet runSearchTest(String input, InvalidReplyCodeHandler invalidReplyCodeHandler) throws RetsException { + SearchResultProcessor processor = createProcessor(invalidReplyCodeHandler); + Reader source = new StringReader(input); + return processor.parse(source); + } + + public void testSmallResult() throws RetsException { + SearchResultSet result = runSearchTest(SearchResultHandlerTest.GOOD_SMALL_TEST); + String[] columns = result.getColumns(); + assertNotNull(columns); + assertEquals("column headers count wrong", 1, columns.length); + assertEquals("bad column header", "Column1", columns[0]); + + if (result.getCount() != -1) + assertEquals("wrong row count", 1, result.getCount()); + + assertTrue("iterator should have more", result.hasNext()); + String[] row = result.next(); + + assertEquals("wrong row width", 1, row.length); + assertEquals("wrong row data", "Data1", row[0]); + + assertFalse("rows should be exhausted", result.hasNext()); + assertFalse("max rows wrong", result.isMaxrows()); + assertTrue("search not complete", result.isComplete()); + } + + public void testEarlyCallToIsMaxRows() throws RetsException { + SearchResultSet result = runSearchTest(SearchResultHandlerTest.ALL_TAGS_TEST); + try { + result.isMaxrows(); + fail("Should throw illegal state exception"); + } catch (IllegalStateException e) { + // "success" + } + } + + public void testAllTags() throws RetsException { + SearchResultSet result = runSearchTest(SearchResultHandlerTest.ALL_TAGS_TEST); + assertEquals("extended count wrong", 100, result.getCount()); + + assertTrue("iterator should have more", result.hasNext()); + String[] row = result.next(); + assertNotNull("row 0 is null", row); + assertEquals("wrong number of row[0] elements", 1, row.length); + assertEquals("wrong row[0] data", "Data1", row[0]); + + assertTrue("iterator should have more", result.hasNext()); + row = result.next(); + assertNotNull("row 1 is null", row); + assertEquals("wrong number of row[1] elements", 1, row.length); + assertEquals("wrong row[1] data", "Data2", row[0]); + + assertFalse("rows should be exhausted", result.hasNext()); + assertTrue("search not complete", result.isComplete()); + assertTrue("max rows not set", result.isMaxrows()); + } + + public void testReplyCode20208() throws RetsException { + SearchResultSet result = runSearchTest(SearchResultHandlerTest.MAXROWS_REPLYCODE); + assertEquals("extended count wrong", 100, result.getCount()); + + assertTrue("iterator should have more", result.hasNext()); + String[] row = result.next(); + assertNotNull("row 0 is null", row); + assertEquals("wrong number of row[0] elements", 1, row.length); + assertEquals("wrong row[0] data", "Data1", row[0]); + + assertTrue("iterator should have more", result.hasNext()); + row = result.next(); + assertNotNull("row 1 is null", row); + assertEquals("wrong number of row[1] elements", 1, row.length); + assertEquals("wrong row[1] data", "Data2", row[0]); + + assertFalse("rows should be exhausted", result.hasNext()); + assertTrue("search not complete", result.isComplete()); + assertTrue("max rows not set", result.isMaxrows()); + } + + public void testReplyCode20201WithColumns() throws RetsException { + SearchResultSet result = runSearchTest(SearchResultHandlerTest.EMPTY_REPLYCODE_WITH_COLUMNS_TAG); + assertFalse("iterator should be empty", result.hasNext()); + } + + public void testReplyCode20201WithoutColumns() throws RetsException { + SearchResultSet result = runSearchTest(SearchResultHandlerTest.EMPTY_REPLYCODE); + assertFalse("iterator should be empty", result.hasNext()); + } + + public void testEarlyException() throws RetsException { + try { + // Test now checks that the error is thrown at process + // or during the evaluation of the data rows, since the + // result may be lazily evaluated (streaming) + SearchResultSet result = runSearchTest(SearchResultHandlerTest.EARLY_ERROR_TEST); + while (result.hasNext()) + result.next(); + fail("Expected an Invalid ReplyCodeException"); + } catch (InvalidReplyCodeException e) { + // "success" + } + } + + public void testLateException() throws RetsException { + try { + // Test now checks that the error is thrown at process + // or during the evaluation of the data rows, since the + // result may be lazily evaluated (streaming) + SearchResultSet result = runSearchTest(SearchResultHandlerTest.LATE_ERROR_TEST); + while (result.hasNext()) + result.next(); + fail("Expected an Invalid ReplyCodeException"); + } catch (InvalidReplyCodeException e) { + // "success" + } + } + + public void testEarlyExceptionWithTrap() throws RetsException { + try { + // Test now checks that the error is thrown at process + // or during the evaluation of the data rows, since the + // result may be lazily evaluated (streaming) + SearchResultSet result = runSearchTest(SearchResultHandlerTest.EARLY_ERROR_TEST, + new TestInvalidReplyCodeHandler()); + while (result.hasNext()) + result.next(); + fail("Expected an Invalid ReplyCodeException"); + } catch (InvalidReplyCodeException e) { + // "success" + } + } + + public void testLateExceptionWithTrap() throws RetsException { + // Test now checks that the error is thrown at process + // or during the evaluation of the data rows, since the + // result may be lazily evaluated (streaming) + TestInvalidReplyCodeHandler testInvalidReplyCodeHandler = new TestInvalidReplyCodeHandler(); + SearchResultSet result = runSearchTest(SearchResultHandlerTest.LATE_ERROR_TEST, testInvalidReplyCodeHandler); + while (result.hasNext()) + result.next(); + + assertEquals(SearchResultHandlerTest.LATE_ERROR_CODE, testInvalidReplyCodeHandler.getReplyCode()); + } + + public void testTimeout() throws Exception { + int timeout = 100; + SearchResultProcessor processor = new StreamingSearchResultProcessor(1, timeout); + Reader source = new StringReader(SearchResultHandlerTest.ALL_TAGS_TEST); + SearchResultSet result = processor.parse(source); + + try { + // attempt to force timeout to occur + Thread.sleep(timeout * 10); + // hasNext should fail b/c timeout + // will have occurred + result.hasNext(); + fail("Should fail since timeout should have been reached"); + } catch (RetsException e) { + // success + } + } + + public void testIONotEatenException() throws RetsException { + SearchResultProcessor processor = new StreamingSearchResultProcessor(100); + + IOFailReader ioExceptionStream = new IOFailReader(new StringReader(SearchResultHandlerTest.ALL_TAGS_TEST)); + ioExceptionStream.setFailRead(true); + + SearchResultSet resultSet = processor.parse(ioExceptionStream); + + try { + while (resultSet.hasNext()) + resultSet.next(); + fail("Expection an IOException to be thrown during stream reading."); + } catch (RetsException e) { + e.printStackTrace(); + assertNotNull(e); + } + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/TestInvalidReplyCodeHandler.java b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/TestInvalidReplyCodeHandler.java new file mode 100644 index 0000000..3406db5 --- /dev/null +++ b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/TestInvalidReplyCodeHandler.java @@ -0,0 +1,20 @@ +package com.ossez.usreio.tests.client; + +import com.ossez.usreio.client.InvalidReplyCodeException; +import com.ossez.usreio.client.InvalidReplyCodeHandler; + +final class TestInvalidReplyCodeHandler implements InvalidReplyCodeHandler { + private int replyCode; + + public void invalidRetsReplyCode(int code) throws InvalidReplyCodeException { + throw new InvalidReplyCodeException(code); + } + + public void invalidRetsStatusReplyCode(int code) throws InvalidReplyCodeException { + this.replyCode = code; + } + + public int getReplyCode() { + return this.replyCode; + } +} diff --git a/rets-io-client/src/test/java/com/ossez/usreio/tests/client/objects-missing.multipart b/rets-io-client/src/test/java/com/ossez/usreio/tests/client/objects-missing.multipart new file mode 100644 index 0000000000000000000000000000000000000000..d3baaf3771a8b646507c5a7290fd88cfbf55bdc7 GIT binary patch literal 105875 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 N|D+;Zw@y^({{Wx + + LookupName VisibleName Version Date + E_SCHOOL Elementary School District 1.00.000 Sat, 20 Mar 2002 12:03:38 GMT + H_SCHOOL High School District 1.00.000 Sat, 20 Mar 2002 12:03:38 GMT + AR Area 1.00.000 Sat, 20 Mar 2002 12:03:38 GMT + IFT Interior Features 1.00.000 Sat, 20 Mar 2002 12:03:38 GMT + VEW View 1.00.000 Sat, 20 Mar 2002 12:03:38 GMT + LISTING_TYPE Listing Type 1.00.000 Sat, 20 Mar 2002 12:03:38 GMT + LISTING_STATUS Listing Status 1.00.000 Sat, 20 Mar 2002 12:03:38 GMT + M_SCHOOL Middle School District 1.00.000 Sat, 20 Mar 2002 12:03:38 GMT + EFT Exerior Features 1.00.000 Sat, 20 Mar 2002 12:03:38 GMT + + + LookupName VisibleName Version Date + DESIGNATIONS DESIGNATIONS 1.00.000 Sat, 20 Mar 2002 12:03:38 GMT + + diff --git a/rets-io-client/src/test/resources/getMetadataResponse_noRecords.xml b/rets-io-client/src/test/resources/getMetadataResponse_noRecords.xml new file mode 100644 index 0000000..6596f33 --- /dev/null +++ b/rets-io-client/src/test/resources/getMetadataResponse_noRecords.xml @@ -0,0 +1 @@ + diff --git a/rets-io-client/src/test/resources/getMetadataResponse_system.xml b/rets-io-client/src/test/resources/getMetadataResponse_system.xml new file mode 100644 index 0000000..5ad7b2b --- /dev/null +++ b/rets-io-client/src/test/resources/getMetadataResponse_system.xml @@ -0,0 +1,6 @@ + + + +The reference implementation of a RETS Server + + diff --git a/rets-io-client/src/test/resources/getMetadataResponse_updateType.xml b/rets-io-client/src/test/resources/getMetadataResponse_updateType.xml new file mode 100644 index 0000000..346b9c8 --- /dev/null +++ b/rets-io-client/src/test/resources/getMetadataResponse_updateType.xml @@ -0,0 +1,7 @@ + + + SystemName Sequence Attributes Default ValidationExpressionID UpdateHelpID ValidationLookupName ValidationExternalName + AGENT_ID 1 1 0 + OFFICE_ID 2 2 0 + + diff --git a/rets-io-client/src/test/resources/log4j2.xml b/rets-io-client/src/test/resources/log4j2.xml new file mode 100644 index 0000000..ff28157 --- /dev/null +++ b/rets-io-client/src/test/resources/log4j2.xml @@ -0,0 +1,97 @@ + + + + + + /home/logs/reoc/services/ + + + + + + + + + + + + + + + + + + %-d{yyyy/MM/dd HH:mm:ss} %-5p [%c] - %m%n + + + + + + + + + + + + + + + %-d{yyyy/MM/dd HH:mm:ss} %-5p [%c] - %m%n + + + + + + + + + + + + + + + %-d{yyyy/MM/dd HH:mm:ss} %-5p [%c] - %m%n + + + + + + + + + + + + + + + %-d{yyyy/MM/dd HH:mm:ss} %-5p [%c] - %m%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rets-io-client/src/test/resources/logback.xml b/rets-io-client/src/test/resources/logback.xml new file mode 100644 index 0000000..8599e4f --- /dev/null +++ b/rets-io-client/src/test/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/rets-io-client/src/test/resources/login_lower_case.xml b/rets-io-client/src/test/resources/login_lower_case.xml new file mode 100644 index 0000000..c73ad68 --- /dev/null +++ b/rets-io-client/src/test/resources/login_lower_case.xml @@ -0,0 +1,17 @@ + + +broker = B123, BO987 +membername = Joe T. Schmoe +metadataversion = 1.00.000 +minmetadataversion = 1.00.000 +user = A123,5678,1,A123 +login = http://rets.test:6103/login +logout = http://rets.test:6103/logout +search = http://rets.test:6103/search +getmetadata = http://rets.test:6103/getMetadata +changepassword = http://rets.test:6103/changePassword +getobject = http://rets.test:6103/getObjectEx +action = http://rets.test:6103/get +balance = 44.21 +timeoutseconds = 60 + diff --git a/rets-io-client/src/test/resources/login_response_valid_1.0.xml b/rets-io-client/src/test/resources/login_response_valid_1.0.xml new file mode 100644 index 0000000..bb2cb2e --- /dev/null +++ b/rets-io-client/src/test/resources/login_response_valid_1.0.xml @@ -0,0 +1,16 @@ + + Broker = B123, BO987 + MemberName = Joe T. Schmoe + MetadataVersion = 1.00.000 + MinMetadataVersion = 1.00.000 + User = A123,5678,1,A123 + Login = http://rets.test:6103/login + Logout = http://rets.test:6103/logout + Search = http://rets.test:6103/search + GetMetadata = http://rets.test:6103/getMetadata + ChangePassword = http://rets.test:6103/changePassword + GetObject = http://rets.test:6103/getObjectEx + Action = http://rets.test:6103/get + Balance = 44.21 + TimeoutSeconds = 60 + diff --git a/rets-io-client/src/test/resources/login_response_valid_1.5.xml b/rets-io-client/src/test/resources/login_response_valid_1.5.xml new file mode 100644 index 0000000..200cbdd --- /dev/null +++ b/rets-io-client/src/test/resources/login_response_valid_1.5.xml @@ -0,0 +1,18 @@ + + + Broker = B123, BO987 + MemberName = Joe T. Schmoe + MetadataVersion = 1.00.000 + MinMetadataVersion = 1.00.000 + User = A123,5678,1,A123 + Login = http://rets.test:6103/login + Logout = http://rets.test:6103/logout + Search = http://rets.test:6103/search + GetMetadata = http://rets.test:6103/getMetadata + ChangePassword = http://rets.test:6103/changePassword + GetObject = http://rets.test:6103/getObjectEx + Action = http://rets.test:6103/get + Balance = 44.21 + TimeoutSeconds = 60 + + diff --git a/rets-io-client/src/test/resources/login_response_valid_1.7.xml b/rets-io-client/src/test/resources/login_response_valid_1.7.xml new file mode 100644 index 0000000..41b86e9 --- /dev/null +++ b/rets-io-client/src/test/resources/login_response_valid_1.7.xml @@ -0,0 +1,17 @@ + + + + MemberName=BHHS Verani IDX RETS User + User=test,1,21,279117 + Broker=4935,4935 + MetadataVersion=19.9.17332 + MetadataTimestamp=2019-09-12T00:52:11.2Z + MinMetadataTimestamp=2019-09-12T00:52:11.2Z + TimeoutSeconds=7200 + GetMetadata=/rets/fnisrets.aspx/NEREN/getmetadata + GetObject=/rets/fnisrets.aspx/NEREN/getobject + Login=http://neren.rets.paragonrels.com/rets/fnisrets.aspx/NEREN/login + Logout=/rets/fnisrets.aspx/NEREN/logout + Search=/rets/fnisrets.aspx/NEREN/search + + diff --git a/rets-io-client/src/test/resources/login_response_valid_1.8.xml b/rets-io-client/src/test/resources/login_response_valid_1.8.xml new file mode 100644 index 0000000..41b86e9 --- /dev/null +++ b/rets-io-client/src/test/resources/login_response_valid_1.8.xml @@ -0,0 +1,17 @@ + + + + MemberName=BHHS Verani IDX RETS User + User=test,1,21,279117 + Broker=4935,4935 + MetadataVersion=19.9.17332 + MetadataTimestamp=2019-09-12T00:52:11.2Z + MinMetadataTimestamp=2019-09-12T00:52:11.2Z + TimeoutSeconds=7200 + GetMetadata=/rets/fnisrets.aspx/NEREN/getmetadata + GetObject=/rets/fnisrets.aspx/NEREN/getobject + Login=http://neren.rets.paragonrels.com/rets/fnisrets.aspx/NEREN/login + Logout=/rets/fnisrets.aspx/NEREN/logout + Search=/rets/fnisrets.aspx/NEREN/search + + diff --git a/rets-io-client/src/test/resources/login_response_valid_1.9.xml b/rets-io-client/src/test/resources/login_response_valid_1.9.xml new file mode 100644 index 0000000..41b86e9 --- /dev/null +++ b/rets-io-client/src/test/resources/login_response_valid_1.9.xml @@ -0,0 +1,17 @@ + + + + MemberName=BHHS Verani IDX RETS User + User=test,1,21,279117 + Broker=4935,4935 + MetadataVersion=19.9.17332 + MetadataTimestamp=2019-09-12T00:52:11.2Z + MinMetadataTimestamp=2019-09-12T00:52:11.2Z + TimeoutSeconds=7200 + GetMetadata=/rets/fnisrets.aspx/NEREN/getmetadata + GetObject=/rets/fnisrets.aspx/NEREN/getobject + Login=http://neren.rets.paragonrels.com/rets/fnisrets.aspx/NEREN/login + Logout=/rets/fnisrets.aspx/NEREN/logout + Search=/rets/fnisrets.aspx/NEREN/search + + diff --git a/rets-io-client/src/test/resources/login_valid10.xml b/rets-io-client/src/test/resources/login_valid10.xml new file mode 100644 index 0000000..3c1b834 --- /dev/null +++ b/rets-io-client/src/test/resources/login_valid10.xml @@ -0,0 +1,16 @@ + +Broker = B123, BO987 +MemberName = Joe T. Schmoe +MetadataVersion = 1.00.000 +MinMetadataVersion = 1.00.000 +User = A123,5678,1,A123 +Login = http://rets.test:6103/login +Logout = http://rets.test:6103/logout +Search = http://rets.test:6103/search +GetMetadata = http://rets.test:6103/getMetadata +ChangePassword = http://rets.test:6103/changePassword +GetObject = http://rets.test:6103/getObjectEx +Action = http://rets.test:6103/get +Balance = 44.21 +TimeoutSeconds = 60 + diff --git a/rets-io-client/src/test/resources/login_valid15.xml b/rets-io-client/src/test/resources/login_valid15.xml new file mode 100644 index 0000000..b2d3ea6 --- /dev/null +++ b/rets-io-client/src/test/resources/login_valid15.xml @@ -0,0 +1,17 @@ + + +Broker = B123, BO987 +MemberName = Joe T. Schmoe +MetadataVersion = 1.00.000 +MinMetadataVersion = 1.00.000 +User = A123,5678,1,A123 +Login = http://rets.test:6103/login +Logout = http://rets.test:6103/logout +Search = http://rets.test:6103/search +GetMetadata = http://rets.test:6103/getMetadata +ChangePassword = http://rets.test:6103/changePassword +GetObject = http://rets.test:6103/getObjectEx +Action = http://rets.test:6103/get +Balance = 44.21 +TimeoutSeconds = 60 + diff --git a/rets-io-client/src/test/resources/logout_lower_case.xml b/rets-io-client/src/test/resources/logout_lower_case.xml new file mode 100644 index 0000000..cacd066 --- /dev/null +++ b/rets-io-client/src/test/resources/logout_lower_case.xml @@ -0,0 +1,6 @@ + + +connecttime = 1000 +billing = $20.00 +signoffmessage = Good Bye + diff --git a/rets-io-client/src/test/resources/logout_no_equals.xml b/rets-io-client/src/test/resources/logout_no_equals.xml new file mode 100644 index 0000000..765386f --- /dev/null +++ b/rets-io-client/src/test/resources/logout_no_equals.xml @@ -0,0 +1,4 @@ + + +Logged Out + diff --git a/rets-io-client/src/test/resources/logout_valid10.xml b/rets-io-client/src/test/resources/logout_valid10.xml new file mode 100644 index 0000000..e3bf94a --- /dev/null +++ b/rets-io-client/src/test/resources/logout_valid10.xml @@ -0,0 +1,5 @@ + +ConnectTime = 1000 +Billing = $20.00 +SignOffMessage = Good Bye + diff --git a/rets-io-client/src/test/resources/logout_valid15.xml b/rets-io-client/src/test/resources/logout_valid15.xml new file mode 100644 index 0000000..dd8864e --- /dev/null +++ b/rets-io-client/src/test/resources/logout_valid15.xml @@ -0,0 +1,6 @@ + + +ConnectTime = 1000 +Billing = $20.00 +SignOffMessage = Good Bye + diff --git a/rets-io-client/src/test/resources/objects-missing.multipart b/rets-io-client/src/test/resources/objects-missing.multipart new file mode 100644 index 0000000000000000000000000000000000000000..d3baaf3771a8b646507c5a7290fd88cfbf55bdc7 GIT binary patch literal 105875 zcmc$_1yEeU)-F0^aCdhNGQi;OPH@-Y&fp#hkU)YBFa(zb3+^&_@WBc0Zb5^4l9zLS zeebVVbUGy%Ro$!iuCG?_)l0e?2NMJm1OQ=`EML=Ylx47y*Ee^V(Ve& z3~>~r7qM~n{jUM9>>-Z6PGa~8I7&*S20@AzK;^#7-{ zZJZ(2-a-F0loS5%{0j|1AF51mLSFsVV`G{?@d=74YXbfbNx| ztOC8hy{!|(!_C9d+1`g<6=KUxZwTh*1q1%90Nw-8kWo-kkkL?4P|?uP&@lhEi_5F)o7=nlhev|HkY6Ap1Ci1G0U7BVJa%U&9>vzXkd~G5;szKivQVbflyge`kUL@PYsd zNPzTb8GsE$`r7~!0HgsP-5%s6QGyr6N5VC4ch-E4F za3K|QU)nwzpDIkx-Qd?X$-`=&vVG#_+!FO?esi0Qy6_a4+$(>N+`r+Gny4D>R`9_aHf=}9Y#-QqW{U#B7I7m zI*GB~pk)V5r&|LBI3it4kLt{F`@+!|U`rIpFop0Ou`&dWrxCo$Pa5d!iuDKZA$j;M z=4*=mO#A3Pk}08~O%&v}0TfkUxPsI~W^XNC9nReP9zb?(q~9A(uMhu92>O+3!=y*d z$iwgYyb{iQ-hzQxSRuzae;Sw%7&>2G8t1~MTT+{tSSYtra6&U-{5?kCnpQUv^;5wy|YxYN6# z-jm~U7be5|wW5r#s8OCT9*hc(8&**lXSPfgL-X~$gG9(Dk4R{l+J1gTH+`B4az^4${BkXM#FoX7V}jDAMGJE2FE3M{uP14PhIJrf?0^0Z>@Qz% zsz-QM%k9{@|3doR#4hp$rSMP&g&3goze5-fWoY?5zPgw)_OKRG&tq`pklRx=z~hl6 z`RCyv_nqH})Eo8Yw&b46rTvX2PXL53P^w8kzY11I1T%&Ee1>kh6~JMVO6y?z})nTBY?y%5u7gZc}d?F<^4Miw~Csb~2x^?Ue8II^9& zZ5|&QoaEb9cKcI;&62|`WFE!S0=xeOfEnBItD-}(eSW=BfMd}YBpDhMv2e_nfP!#L zX9{uw018&_7wpTxpE_iOL;BS3k8iHO@+tb+bW^8_FwJzn6#qa$SImH;#Y=*W?Y{f; z2aq$v40&) zC-kf%UyS!T5f8a59H>2fbolIw6S*kR9d;3kB;Y1=zSx7te6B2OQrh}7&EACPM*;4b ziLzg51)$;NKs)A!xLKYo8u;ovAQ<$C?HkvnIwyMR?8idFX?Nc-D)&`wCxjV>CcO+% zfd3R5z5!>D!_%u%AI3^>twQgPXwR_pN8;`}i9xm)e^T5PZ*eb!j;5y8^*iZf{_uOC zM6hjbJ`V1TkA#$OH&-!q^ZgD};x5P1gpxERAZBcP>jh)RNAVcjQ*S5)t3Ue* zrBaSvQD`0(j>;#6C%4b~R@eHL?Rl+kWIe`d_%F8ta{P$_qwm;Y>zcxwr{knETy>m@ z>7tuYekJb6>c&|)go-rJZr#P*e1p<$21>Gzn_2kdd2<#UewWN16i8-c78|^WP!x17 zNq?${PGS|JcQ?n+Wf%N>uO&Glo;sdHZs0w@}Z?7+YZ$I@NTP|5P!%4nd=CgagQr>ytPwq8#;O`Nxjm65NI zgbo3s>Rkb-_RvHrLUVmdRbU!3in zmy?n)2*$YeP>}$%jZ?a*AAM40o|;rGh}<<>k3WVegPoh(0k-0OeYn*H?{T@ zSY2!G=TvP{0nr1|Yt$Nqb8xeqcck&w$J3Xx0Pj%AV^MJ_Jx;+2;VwGYv18VRvObdU z;hc;8hx2lD%rLKr(@ANyypUgP%EH=f#xdObR@u=g!jAU?gu>d9kX}F3S>O z+y)=+3X;{@xbb(MO*ZIuyY9b2zv<*n+>Y2fmi!RD@My;(9{xCsVzGtduPCLTnY-=I*Vi}B`q`eje)UaJP%^uy1U7Z>X0Va!`A;3#j!|c{tA4bdf(f#7 zBQ@k9u}f3(DywxsiE~Wfev)lBfHAF3m;gFuUK3l5UEv{>%cSTzgJm@G2T-xr>T@$m z6EfGQq3ZI;IG9Yb9`#icnlJqs8=P*Ir0U-KLzyl9F>5~g^hT0fkxqiB-=K9+o<|FU z!`3+x%tR)Ww&uG6q!M0oJ0&!<538p|myU&RyM~!$p~KtAM7#9|ckU^v{ZOx8%G#pc z?ydE>U#e1~TILxe09P9jS3^Pc2z_^eCkkut<`@|TLl8ppPR3Ox|(i zO|$5C^k_nNxobaN z(kYj+jV1utD)}QyXuWjA-NdYdx9Sa9=&uIi;h@_PC*_Rv`3r$1gl*%VECqGEtq5xy z9e!ZC+zhw%w8Qu9&cGK4u9V(bgm*kap7%=Y;YKI%mxrR=Yq=_&L2#^TL+)IDb64J0 zjmKy8ETI*5Am&p4m9 z#StIvsG70~)1!3C*+>l^-6$5PPmKc4W`?0V@>mFR>ANVsC3WzF~cUbP`tyohV%Y90sm zZld`nNh9CvunBriVLVV8k-?hwQU_w3b@$|RzM`ef?zD;^zDZrDWLHr9%*G*BR;&2k6pVHctoOdg2Cc;-hN0v?y%2 znv-1mQ^)(XOK+*mW*Yfq=Z4L%;Wrviyy^CeT=6dQS^2ECgf`QJZ4+zyFPPT<@P-*~ zzw!do2qHJqob&U-n30nW?m@Sb{wQ zg6%v+9aIe#9jlE11bHphwkFMO*gzhL}v7c*#NSkO}d|6$H%KQi{3w zr&Ue=g7YsIGkd>WZB0865jBEXLhIXuMzI1E4@-eM&~L&3-MDtfw|H$nZAx)W%g+8} zgWD6mhla{cpFcSFVt)~N&_^LhL*9Hc(%I@XdeYt6vx5;uBAcVoyFau!O6=6wYAWRt zytfpcEN1mX(lYKxpl-LvJPyAP<9r7#UaNo*s@vZEvy5J zZIn@I?;Qz(L)jbAX>U5YO%%deNhF~*;6L_T^KQ}r*&DnPOB+@EjIey43W!%Dw+XFO!f8>d_yE8lDz2&o?)4}h zv9nNc{CEC!^?ecjcJ@xp9jLMl24odUiT3@RIDuhD(5PumOyh7Rw@pJEG{6qLPp>(x zzqyN9F_=*5?+>5R0A1A|4!*d zs;;5AGUP1r4b)c_(-6@PRSH@78pz*!9_TP_yw#GmuR4-xY3?m^8lm^Hy-kw8M;3xgcre1I@*aG(tV6x4G8???>K}_B&!Z}I=#ofC(o~~x+ zqpcnqJLfpUD^Yd!k6PrV4wiJ5ZR2Fxp!GPg1zmPxBX(agt!fiXvy4y7-(-P ziQAtCU!yGu;y1LPxZ15HYi-|?Wl5&y=!uOjd{>F_7%O_=t&ewbY8I?n=t`QE9zbBe zG(gja>Y5y0UKFJpQa2Gu`>x6?>_jKWV^Kxh_2ZpUu&b$p9=^#7I_am_8);kp2O)A+hz;md8 zd4#b0MKIocj=@@_=FoOig3H;&ltugSn1RR>5jiMf5D=#{}n{_vsi;Kx@8PtzF zX=;rjk)NJ%rky_?B?kFyW10BBK4OVoUa4=0;*xru9gi#hmHjQQm>($QGPKg;=x(@Z zBT(D*&SgfR=4sgTD<6~9N=6Q?N$CAf7IadlW0v%C)Krxzg4u-2*6$U0v{$N5PDJD8 z0ZP}W+`k!!KY(z(2FRp>|Mra~mlj^qQ*WiW5SDKbT}Ihvf~jP$7P!`E4B0NkPXC0j zIoIKrfjo9O(P#*H?BY+}M z;}!`i-X5&2P*II$9othskotu`HCv_Pb#8jK`Ro#%}$E$sw{F!S@RQ9wXl8PAY#!j z`O)ar*VgLzLekWj>qL^ku!#xrP`R3u`z3Gm zpQ=rveMS|W8!orOzMf6jpY8c`*zK7ezbAJ;yHGTpin%79JefTObS9IRbDq5f=QN9j z3BIXU+hwnU${WmAb%4M#wos~4iQ>_hNQQcYmqoebGx?=W>Zjf`tViqf>Y`N&axz*Y)` zYH_y491$L-1*F);+|l!6@hQMiq;k*kGXIdk zmDnF~EF1YL+wRg@(Z?Dp37-smPfySCUh>)|RRr08pbrl`cv|0YfDHTr+#XIV;LGwh zWtgq@;Z7Jom<>&!irxIwbTxZV6_Rjqm%t6W#&Rl1nap~m@LkgT9b}&IJrey$b>MT5cAM>pdL^;34>~JM9m-~UDyXUVbYH)kx z!ZBQ8XTagEbl^@JV@iHNylg3Q zNAZgHF9FJ6DFTH3#eIwLP9MJmBZdxTzM%ZghNuC`%2$I(t4BE$o>Yw>pAU*{s6KJS z3F$NI+9#c4fnCb4E=)8H9g?BZRndhWq4ozNmpjaZx|&ELbWz&y+IJ^5CTB%Wv8VFH zSAgybeTWpI*d;SX<8jBWcwbD~P)}$VH;N#96${Ms;{I*pPfSy)SF-Y)HDXntPdK z%$uWZIj3$|L|Rz${@Sh3_&DElKQ~a{ z4by&Nt+8H_J9^FFq^~l${{bn|j#Pel^C_$;y?ktDII}Q*m;y7xBw`|z*XOUvDsbJ zrSzqKl5NQ7XM7rfXi!p%WccWDJcsTunB~0bTSkLLw!DCnTP^*YZhUEinM2D2)yAsF z9G%$L=e=?7XRF*#88QT>$^x9pU_LM@^IZVP(_81B60z(GjfA;eN-fc7{T%nTyiR2{ z!2`95*4e@mJ~EX#>H3;=bAn({05h|?9WKF z5z#6R&8e2z(9>6DLvFrHW_sZrfj2=485Jz&jwMcT}5{s-XP zoJlNHhtjH@z1U)~24}XSd##r4cN(1qvzuf+ci5O17^I@UWo#t)!b)qoEp(BmrM4&d zEhdj{rsGG|*}&=88@C8m^PToztHD?IhK+}6VIL&j6r7(rEfHz=+1o-j<*S4X^(MWt zuN#)YGD;zEHp!L&o5lOcJ1ylW>IHw518(=PPQ#CzR|_}QT}}hu2#>Mt1T?lu$v*%d z`L9!9O!skG2Kt?`kJO$#Ne#H4`_ZK{1(yz`$++x2v#N8woO8l1SqW>pl(D6mOf+*v zpK-_5NjUZH=N8ib03K!`47_pM)K4J3PuU3$A3;>0bt@g*%qi@UhB3 zib)#jR67?#IA1+yA4VhvycF*pw-(k+E4_YW#ti#WGoi5t^|E^@PG3fRI3t`*;$nf? zky>A@c(?gCb*zT29`!(KA#(I@qG6^)%g<5DoE|VHZyoco?e(+EGvyHM#qN85dtLq& zCQ(t=;^q%&wq8_xz$*?RbvnKH3(I)=d@81BjS(aJZ1yUh-pLRSHKfa8jh;f0BRq4W zK@{IdvUuxA5zrf1o@9w4bXGeCR=pXvBs{bl8t|c>h$s(Vz93N*>Zc3y67A_Nc|s+! znkGh->h`2)+F7L6+Ks~E(!n}LqULsVyi z5bI3ibFr~60!{MCaIF!QMSKsYt7Rm;YgoO8ZgiwEo!{00Z$zrDW{A>~XNsCU(A8e@ zINip!n3%KJEr)@RyDMEM3FT>~i(u$vu6#M9xGmkb(7xm4DAz@<6N8j~{1#hgIR|r^ z+tI}IwKzM282eTy!#kNQ=RTT5;%V?=@Gh#Ldok5Dq3Kd4;jyp=r(%d-%;ZQue!1VS zV_u;^WBe%v^OlP3 z269$sNQk3QA+@aBDbPl~F&_Jdp%(8Cz%*w!OC^;w6L}#vfeVjdQOO1mk^b4ub|GuR z^}Vve8dd%CH<4AmeHe9BsvTU!8QsNdlP3N`F0EHQV$vC8P)psy6u2xyVGVl#J6qw?4YI1TUNbv z*ohe6tvb}Rl7sj93%O9*yoTpw)qsy9$}}Cfimk57W(uqYSR!kFA0spuh2@gjEPDb zmUv25UpQiB9kWX{G{M(OlArGqV3Uy$-I1$FXLW9 zeZAeZo0YKip>3tk#`RO}eB(9T0K8QGsv5y|psndOi%lz1y?$k6fwgs=vdG^0P)-iYC9~)xHG!)=yhnnTsVdpp|vBRz0U&|TIn*RQ+-`L#Tf=)=m&0-#|231`J zZxq~?3UZFiO{ny@b_xv@jwbMRSVu7Z^ohumCeQ^@9Jx%YbTmBZw&@`7=5f4P!g>84H0LFfbbGQyc*0ORl_ z5z$9Hd6VIG97c+r=C)=&Ew55#u0H@yRlJdkya_&Uq?<|xF0Vxd@>|EG@M`;LC0rTN zhYy<00x2}tt#w=DA^NTD9h{*r7MI_9Yz!Kgw_1T4?LMb3bZWTn+*keNww^UKNj68f zi(dTVHK9ongU}2;E)x-F${kVH;SQq@(*6S|aK2od=zr7RtuR?(72=gRm|b>s;VN`N z(TloVUWZLnxrCFP^Jx1xJF62rm8z-hVQaJTi*{q=n=Q3F1ysO*Yw-~p61(rfkJWOZ zvemL24W@eON#tzK7jV3ARc#rfcA#czJWjfx#V?ERv5vED%8I(ye@$n)pkWFU!{Bn@ z!%}u@OlDZuB0oruM>@QzNKmif167S|JA*batG3p;RWA?HYc!=S`tCoxnKg|X$<o)=Yn$XK&y;&}k*pawJBIaIe_sL<`FgxqA$#2zI=Un$>avEg}2Z z^^>s$Pz(cCimB&~7?&4kTG}%Wm7P~k(e|jwJRcq7^_L~%%&Km#PwYd-3F4@rn@DK1 z2XwhUBN{I5G`=)|b^>LgVte-&6cR1lew^(!EJ~7+93V~S^ZA%EcL;^Mj_luU4Jpfa zt65QGu7#><%ZyQ*ye3>2CB;v5tI1p0Y+p5GJ!;G+y=^iLV1endfF%lRCtr9SMiKFN7 zVFKhykR7@F@h+-2+&-Bl>cu)wn4ZK@DwrCqCkv*2&D~!c`9P>heuW>=^Tv-RBqcUR zLhvog{&9t`84J}b8#!t_uPA0hRAtfUV?omS>~z=B%oC48OIj!n_Y#DsID?dR8Hx9b zoM=!7ijKxu)L1tyIPPG~Rr$3EjK*Jll!X5-9$Wa;u3uG1@J~TeE${KcISg)gz=-4dCcoF1{fR*5=3__f%lDV$^;;Lo$G;bv?0zhBOKSL< zuE>GpRX%Gprk}tsCVJ$?DZ`5ht{iCscXDoTD?y^w(v2WLs@8VwO9ow=%Z=zta* z{%(&(?!VkON@%5$tFHyZz?Zn|jkB$gr8m{XH1^f;Ns@a_zdGlJbGS%-c*Sj!5u!am zuy{m7dJd=I*}adD?4k}E&g=>4nBU$9;Y)O%<;|3YR5U2`Oib<#H6%f2qoc&t(G+`_ zj?!;Mdp1ceHwMfP5}yMkJMWf8SPJ|3KbrFnQ=hRYYrWEohdj91T9$BhXmQRzD?K(H zM|5r75sGexqPfHfjJ8kL1aJ>fBkTP3GMlRzrPcS*?HlCDQd)zi&~%uAb`~)&Rfem~ zLg+sEOt+G@-4)q_Jg8Y&hu_Q{t4tDL5~jWJnm!pNu2 z%);3ZGt&oniLX>bTf0j`zs*0%eL8}7o#St)9nWmz|XLJ{@d5Nh_l z+Pgbw;?^p-21-#JOJ;nMGrVuR_mGb4*yTE7KfC3`)Hhf~PgOtqvFEs*wJzPZJonV! z@i?pbv-68#?B?-QtLiU&HJZ30lM6IB8j$(mu=i2>iZ~QG0wRVf_Y-r72NDd@4ZW8~ z)58U?)eKFNUX7Alx19c3()A9VY|@^AOrQGfzaC)yndYp@VvL(ZG|0cY0CIjBOu7$! zm20|vdSg<}m?oYv*2j>Ws!C4%2oeOSHwhI?f?_qIh56a`BHdcjPMXiK;!b zrl`ZTANz)GK`#7}{5|yEXyoOu9WK8prELzvr5w+C`eS*GX`lb@cg(u!L~UcaUdIY2 zeCJFmEnygDi-@m2jo?E6N}{nL=RdpVE(YRWp5uY22JYF6gu}+6{g~4; ze%wqUs1F3WVfS+!Tmh!7N!rb}g|I0{Kso-e0bg;|--!?QQ+-8oP{l zE`u9oy37*aR#C1kY8u5dEojAb2tqFJqcn<3{ozDre-9;iijdQL>eFfC=dApe;` zGOo<^4*cGbFRd+2y5DeS)E4bgaK5s~UqXWjr-d!0A&)osae=0KsrB2;vhr(|?<@S` zeC6IMdhPjpb=PH0xqTrFj{*cgf}`_si*KXJNXy-ep(iAqEoW2_Ow_^@KY|7oxl>Dq z)*&^G%npX?ff+fK%Hbo@aZ&}vU*|rsa~{?Up9-E@>Q7&<@%^Y;CNVP7Ub18UDTqNgV`dWH^UA~g~6n6jye4=_tJg8*vUe$VR;*VL1&`n+OF^g)0o=2 zDH|ep(CIKFZipjT&0Rzl@uzt{}ER}m-l4PCMRmZz&y5>$*>l#b=5 z#{KT>g+eEXkS^9!J-ts*3<$VZ_+SZ#@|r#wcF^FOqNDAWzReZ-8|l|NecPGeHlz!g z==yYV+B9oW;@*fxd?(M)FtcDfyPLMyVc0BmH9(%HxNNSZx=JP8ADor!hZXd_xU0gi zgxcD0U$8YE&#~LsH=35g-4-d~=6(j=vk$Te`zB z`p%@u0fh$FFg-M3SkbCI`ewg`o=HN=hA=!L$0#&|#Lb zGg3U%hOSYOf-3=+;M2tfD$?Pjt~2&N~pPwQI3Oq>-QTKN``zjgxP{D`=qd3$S;#ahuU-iDbV+f5Bg` zrt$+d=x>5fK)0trk$J)~os#+bZ2vYP^REVf6Tm){rmty0rqbYo zCRPJB3E(hQ%RyGBdr;X!y(01L-E+>{;!_Z`$LG-y!`W+nnR?(+;19qpB;+exXz%lo z=Zjr})f>@UL7LptA98{pvIP8ot#)Z%CC@auRoRlBj1r6zd-sWJ2}~87rPz{?O9s=5 zAPzL!XWdz`Ya3_enWAy+r5x5X&ctbFeQ|Ik`If~;7prz-Hwbh2rSL%4hAH8YWJQ^N7Q^_M_-+H5FWv**IKF7=EFoD79Wsv zeDYRpfi+>dr$;I1T!ho}sz2>MQgiulMb8XNgAWnr0biTyI4w-l1%Jn2D_zdKa@t>u ziYO9aiHM#!`@Cfw5F9nW|0PM0+S0XFx43pBkS;_jqXPPv%zC2iTVS{ShtH%0OG;@Ms z@*iZ2`>lP>x8B0t<>=x}cB?X?M`F9UPDXmQ0yzSbSctYLXbEx~1n$UXsJ%L{w+2jR zI=}{SzKj1{Wpn>9{nLErng^dU zq;>?{ROaX=a@rbscQ%XRNcXi_&F+#KVz?45ezFqFoxFsjy?Z|qd|htI9=Uv_(Msmc zXK`5Dz;cGl%tX)d2ot#}e;>@Cw$LsZMuVldS%JYd+UP-Qkt+w=dDji_G@=nhGRYE3 zv~E4wnTN21#ut&{P#A3a{E%UapDkUMDD}8c=kx27y17BxL+q*7V!SB>2CaH}<$Ug@ zj8kt^t+EGBz|O6bX(?20k(7&lW}}u0;+msFquP}#J`$dVsG5+s6+YPIeQ}g(c|Q6O z52-4i-7#ywZGR%3ny4M0H+=9etRcr1pJQor@OBo@i7s}?@-Gf5S?X8i>)?HNzD)!Nik9>;$Pb2>y_< zDyIdQ&yo4L6HWHPC^rx?X6d4|)e!fFIyhzKf%4c?S7p2NNGQM%f14{gT(rzFcKfG6 z-4uvbXnTv&EIUe~4}V8G&PxvvvbZ`UK*O)p1eUd_+w-1*y;(wQr`t0hXdViqcd;gZ zZz1#w`zrC}-Lr;o>5@5d6MxMf|1yFX$ej98(^JxwR@Lk*4^eDuUG?ttwa`2O`v5Ta zS$vPh7?u7!s9au5=?@@rJTU%+Z}zJ{fDN#bL?U7DtM=v+Q1sJ!xIt%sUi3s}nVFUc z&4Y}49EJpIz3GL`YW*_&IEslf~tpGb6F@DD|WVF%Wxt~t;Rc?*mh4dGXz zI{_C;dd+F;y_?8Ii)QKiHDMW?6_N@slYqEg?-1AfAb>PcPKhZ*~#lF8g=z=9Ze>U zX-)oiFP!92&|ABA>mDx*e^Loxye>n@d&je6-`vv4`%2Z8cAw)5c2p1bLAbO8WxgZF z5t!+SY`&I@gILb0k6i0 zER;Q5{R_626|#i(Xs!HhX)qjy*z&HO$9+_qeY4WOJWjw)Y?GgtrYuoDGnbwe)u4ro z>6>c8mNg<^RP0tkF?>bSwxiep`jnnysMvN+YPP`p6a~HIxKKkAcRPLK9zTLt=zs%Y zb!cE^LYVkfb!S!Tt{2s;XngZpzTBoCFrVgau3Ty*jt>a?God0*N3^7jN1IMvQH&>X z6o6aFr<>++*6X#g>5ta7W`%aBBG`IiO#Et?oO$!*^|29-9^bOcP{;ge257uLk@C$J zRjT}tLT3Mp<}UZD&VmW<`RNm^kMp8fg?33sR=>G9!wd_3*abuWp5vSps>B)S2uLbu z)lVe}GXzqp-9A1U3f_M66K4h&U;Bg>E5HfTeH!wqv5{tPB`|m3owoVU!XVjV7qO;y zJ>`-Eo&DI@X4TKzn+H;BA85i-(_KUT3gx{Vxh#G8CR;}Da#3GIt-PB-qj{Owzu?(a zknwbU0DtfN1G1g4;Ke`CxS7|&R)Si8_OmL!&Rr?>&aNe?G`*(!A4k&ioo;ZhCEpq} zq?nxl8mKx=Pg@cG@+n5jE9F`8G0ZP&+u6XV&lU7B&Ds8?Z96qR!HL^J5_l3QVN?Wb z#i=4FF8Gm_7VI}$XSx&=z>U+P@{JMZDfzG?g3z2xEe#CN{cccGiEV^W=8%Ps9K#jT zfQ5&NV>F|{PN1$O!2@G2=`K?H0vv1dZ>lR0q>RVC zl%Lq&_p>TuA=Vzg6|?ZHoA}xbaD%?39J?S~ z9=3Oj@w$y0bf5SrV_;EqDRToX6BH|Yz3iZWXQVrFekhhC^@@SGqo&N|#&O>Y5 zeY~s~V)rp#Ng4g5+qWrX-b@^&(`-=gn5rZd@w1ObCq9!)>#zL4eEH%k?>Tr@d80JW zdoOB|^1h;+YJ+@D9`bg|%7dGp3=%XXf|+@Bxse|Hxz4}KQ;{Vmsjq2A@? z_Ses#4ZVuG4@o7!)oa=tv=28GO0L;uR#p(Rlk;{vNURW3Jg~=m39Y37JybK>pbg&A z_NmZ;1sw?dI9C__5R$d+zVbHFPmvG<=sf5np0cW4L8t1oVC>`(x!%Ly;TdK&3zCFaJm9TF@;4>6oOEMmw z4~+)_T!SQ|w9i@IOBC%wDvYWO3HAo|_DB&FXt4w5Yu_95G`J7f3`Q`HImR~VC5i&7Qtov5;t=iEK8$ISShIc^db_$aR&UCVT ze&)7YA;A%flk?pI73I2k>&_^|0Ppg1sRU)fj{#T8Z!S&WQkBXm`y^C9?k5Cri|>Vm zku>C6nmPZbYl>wDoqdJ#w2#I_u(6R3{XSbSXsOdL9u1^14vrD$n|YWvtyCrKxlPWF z&P|i%mPlyT4jP_>%%xYXFPlwlPvtT4eKoL8D7u3n(ce)20Yn9v-iSY1FlD4#K7s{w z6~?K1Br>To^o%K&a+m;y`x2U|5vd$v4 z?`M}qTz0EQXnDD+&lTx&x*=d06o3!pRcVI#R@j7VHn; zM99$6mKUC&f&(^(8YJG;#WoUKj7u!%-E+qfaOpt&c7VC>oKMjH0LpWIj@y(yvq@GQ zK2}&%mFCFUS?_fhV)wD#`z$!c;8_rTi46Pmjd%LCTVIp19}65m)+MI&vac~eerfRJn;bmwoa2;o!vJFRUQ-- zE;gGpEm=G<9!&L#>IvE-N^|1=TQL-(LZ7ko?k7ul(6U9UWFX(dANMGj4T4)@ocncH*cLBB)fjNjX# zG4r6Ek6VWbjpqat`(hL=biCR=a)xo{o-nO@5+O$+ymIDmdW{D>Bn+Fu<30owSMJ~4 z$fW~Be?s%F_w0##v+lAaG0kcdn6odF5iJqWk}s2;UR|JBcZh_6ebJHOr3ScVwT?vO zGLNQJS5%r04*BwER7~cyM)9)CF0-?6Jwfhrd;R|cdO(H07{1F6nh{bdI6H{ox6=oV z*5oK-g*?I@ML-LBp7dN+w&$IA^FX-Mb#~RXFv&c`JWCS={qp6GbH*3}-=27`iqBuO z@dk%J{YOrTFCuk_35Mvw1P#EP=a71S0-IKnmvUhJ+jcR$F@G+l;T5yg=W-omwRpmS ztsDHUlZ=m-cY5ahIdf&Fsj}7XnF=SF1BJ^vDIlrFMsP>vS=Jq#K zw6t^zxeiLR2A;$15XUZ6q%&n38&oN5ZX?sJa?NutQ)+gN*{R}tx0WF>k}JkC z2$SsPj>1VKU>?1FtDV%0wn9ja#Jg2PktjbRW1cu2>skD2#uE5-Ilm6Qy{+WJ+{(}@ zCX`UO=bvZm(07TJf1%BJRIYX%ZjP3>lZp@rPubA&ArjKK_-4|neq@RW62mSdJco$l?1u$ zg;w!~r=Mt1$*OCr@_hRn&Xvz|*Y!2%I&?VQfz*oLZt^kODH>saaXtvhiShXw%JB8p zi~WJ5&vmZpcK0^coyJI4cl3pM%SS70S+1q zd8T4Vt`H9Xwa!^f>KU_p*wIsYgrt1^e#mwN-I@z)TJe7V@Jc

Df-`vAEgJt5W!@bcn65>$5rgi41VtsQU0~+g8~GZnW3%Bf7V-)t_YA zwz)L2+sikXCZr@)D-$>jyK&SJ#c23H=J39n;Pr)00N`?JCf$g&o`t9ZFhqKMqOi3~ zP1dm9M9h;}mUS!K2F*krq=BoWO3r4~ppNncobTdAg~<8)@%b8=E#-<#wYZNxLB1ox8Ctg=Flp6OZdk#FXB~Rd!Sg*g5NiihzfS!DJ!3 zW|2m`G8rRTBgZ{dcc2Z79XKn(c;U2wf%5iGxowv~(#5GhV_heKX4Cb_ zUcustrro_3T(B7oeoSKk;PuZ+(onj%le4jrt%;euo46XorxUMPVupKw2V7jSrG-svF1EeJ1k~7HwGm9#D-Ikybo^t z*3Q4HwT8D1rpRuOlN@nn$u1azrC1+d>qOIIkGe{9N$h5QPg6;uoH5QN1+qB=HV6ml znr^peD(%$UP8U}9AVUj>18>X2Fm8YWo;!dmE45;zw78mfucml^PPI)!SC#cSJeQ6s zenm{NU%Y_w1A7|5@gMyBz5?aLA5XTzrk8Usu0}q&AX|D1*e>m6gmX zr^2oFcz=Ad#0Vs=4gll<_3c!wZS=8ZHb}DG9iYMc$CPu%!OlVBpRHWBxsq>UnYWtG z<$y*KjD}p0Mt!Tk@LsPa=7n(z%Q3to%aMu@zEY~9jDgp$N{W_r>^P?xXmqyL<4?3` ztpJTAQ@&{=F8N+~CkOK;vP2eF7qg|*K>)}TNQZKB$87V#<07zeR%snKb|Uc3jSiD` z_ZE}KaEjX^TZbzn94XJ;;dmdB=bEXlPj_HN&Xm&KYOsY>2m{A}KJhu{f%?*S(J<0C zXIr0ePBzJ1U-5pbKSr~ko z_U64fy&aZ_&fK$0`q-ZEHI}cZSy{Ejk_1z5P;FUMw&TZtlrZNVMRu*7gp6mfCyIpi zWVF&pA@L7Kn^`uh5U938EYb%6C-+ZKPB7n{a5p+q!xYx`a!vM?iDj5u%uF{3NCA+4 zdnA6fq|=gDGp6}(Vs8jv%Jwno@i>MyU9;n?i^;<0g2WCwbsW~MoOToWso}U~x6~3u zF-d!d0sdo?oPaPhk&~XCE1o>J*P**_p^G$^TBNr+l#3WkW&+|RlpM@>IO)`mKBAp8 zO%lSz93+tj$R9F|%s-#=tfs8)T1wV5G@H99A`N{M@s^30Zc?H{kc*6*{{Rs=$75Og z>q%|l$C4|8@`aUC3O-x|xNaPummPg+Mczo7iqX0_?+II4U0+FaszV&{BJEPlKnFce z(d}M?q&A)8!d$eWN6QpU5ESf39MLs-*qE<#LpIiPZgqV?+(uwqqzM@rZJ`%v2mS|M zZSgBqw^4~!6k<5j;H0V zV#f^?&3oVNyGv5g>SFP8hK@NmK0D+A(!A@(47l9M!dJx> z${U7`=Od0k(WU1m-xwWOey24HLQHJ} zc=WwOT{h-0ZEiV0PYOXG6ZHPI(0MULN~9JDLB|s@BiDmn)Z1||ozo$bTuN8%0I9$t zCBPz@ODYYSj1CTZpTe}*%u;cTZVG#MG^cY7rv&!~fJ0c226KV;ew5PRHzS7RP*sSZ za${k}IT@s8D~xa-hU*QwD)qmG>ny;3{5 zE>`X-56Ih*llV`mJ*rJpR2P=8!R6ve(_=h%ZT#map0&d&ZE4u)O+8Va3A?q1acv}s zi~_p4@YMZ1bsJSTmP;?0%wkyiC^5*$pSF@!tzMjfsKhk zMlvut_EF!`mrd92tZ$m`#$P5!+8D^>q>cf}C-FRcj%ZdBy4aIY@hi&yWBrlj-NULG z0G@UP5ZZl`OM6{O-+E5ZEY`89B?tg6IOs-s^{JfNl8C9T-(x!0 zO_FU*BDH33I!60EZL~8kbA#LN`}VI^@XnB$9+k6EyqVR2l0bz^Z5hUTA94$gdkuAt3Ex)7azbS$aUXvc3&!ME0Ia#y)1;88OHh#t+TV z*E-SNoekYs=>F3-rxmb*?V`30-)dCBB|@$-o}Bis55y6euC8N>2`*!XRx4_(cWnn5 zIO7BQQ8n+`le|u<-^CX8)7xrLU0PZ~kgQj>^SDN0r#VmIEOKx&k~>m-NSeWKJTI%m zVt#v+|a4c=DVx8@yXo9SO?})HvZVCSAKD^P;hOA+Da@yugS;27` zg+;_~BA=YAFaUb=KD{dMpLu5Xv!@dw#1p{>_^U{6Ts{)h*;&|4_K57!Pu?xPus*!|SDSo6ZT>aZ909bOQn=~O zyj8W5OosjBdOel**z~v&uF}z-$^=PV5V3<2jFa`nbDk`U>f6EMOQRI&ajIr1Qb6FZ zmnrHx^!4vhhCAWbou}XEmv=hSNw(JI!fcajvXH!Ezb3P;t)`DmzVi$@eKmb^5@r@QCbe8=vDA6o?ff1G40{o>;iM%I-#cCS-i*lM$Ts}gJ^+PlGn~0>4p&8HKJXQNC;?->8oeij&aI)?I@bBHR z6<1Q)F2RpJ9CXA;4K_kZ465aa-ew(ZqvT*T9;$w39S(RDgq6Vj+cXA)S&ngxW2Zf- zgpcs%fswO+cbbcLQTb-2j7D#iW4EP5*&dia^&{9|eYx65sPLP&JV@ue*s>qqKebaUv0sj1Gdh9VZ>m$Vqo6 zvA5MUy*l^GHj>4A7m>Kh8$4&6um>b?eYy&@rRXuqr#19ioG?6WSlUec3xGKK=bk9m z_a2Cd%S)ve=gE(AKAl<`E_7O>2Oa{uTN=5US+oHgymCIxbWLFHElF|rF^GT09lFSFDGlBV5sa*?ZThwihmZKaO zlZf2LOG^+k?F4b?0nciJ=0tW`U@;A#0T5z)43qeeT1ai^ZD=z|VF8vVw-d67k-lB% z!G;Jp&rI==#Y-l&VQqRZ@0#*U+k8tLP{JHZj&jUffybe&t<5W;-Kd@3zin@2sjb{m zyth`0L6o-dENwhv01S?LcdYww6UV6e*36N$)b3D1)3C~b230^Hf&NWp2;I2$E?(;9 zM@QnTT|MC@>MOoiY88O6$n*f>vJjpNYL1=ua^3z1X z;{@;q@&_k?IO|@F-K6}GqZeelT-MNHyRch(nJi&;m=Mw{DBR4;j#nd&oohmA?;7@4 zZ6SsCCWSK+x`NrO0)|fu1`50JJiDbgdr7Hnob{75&|$))Gwi(c0X>I>hRJ zV8Ff<=N;9${{VW8A~|k%BW5&XwKtN&B6gC;8OLGMx3(*qOWikCT8V6>CVOW&wz!Z) z@`V^h+!$cvjN`s4l%QzZ+}9QaN2kWY++)sU!RH)|j+HRItT{<(BWff~Kf|^&x0Ni# zyyJ$!P=0J;qSv(tvGDcQnQ`SA<$%c21jDN|nZ|N+!SVXm99zZ6`qr>_M9K|K9d4zmn+YtJ$w|ys za1Dfz3G&yjDi0ZIZ>xAJ2A1E*YiqdNC{+qUia~*n+|))zxLnTg=BM@@AyPOu2Ep5r#&Cb7OQzfSPs-VK;k%R10!@Bz{gqanospy=bD+}hG&R*D zv(q#?8&?+B3mo2K$tq$AGlnPT?lN*tX`co zQ9>|>{rB(w(WoZnSX7{Gyb&w^0O&@rwC5TgvmszU&2ha~{eTGmRizhcSSGHlbaFFC z8avM8cScKbw2b3|4?Pd%Rq`a1s1r)3yA8G8lx=$)+My*N4XyI+IM@#)Q0^IWM%?kV z8UTr#``j?#(+dvWJ7m&q5*SE7bks~p1d4)&?)h_yjgMi?05RJ=MKmzL$4U#Z-ZDi; zGKU)&XVH&Q_||ic%Y>X+H-@!vnu}W8+s$&MGO~#=j-2#4?^U(Jk;^C8*5U~Iub79P z{qe83h`+#q520oRPU9~jjAeh~yf-J%} zK_LNw1+&+hhi2KyMQ@Z0@tUx;jhMA)7Q!28tcysH>{*qE8&|6-8SU8BBoXboxQ<=q zf}b$S0E}_@)=E6C=)2umzo=`Ph0@$d_T#ete9auIl7V_{Z1KYpjCZYHhNtZQEw{X8 zMo43E9sxf&91M*0#W+PpwnawTQ9CVl?ymIPyR=E*xrhN2YIkMw+~*m<8LIlEZ)>Gm z!RHfi0Lx8nF?pn8f__oc=E&<@X>y~XyXr;ZTQPT}+()U)C6(LUNhC1L8+bv$&T-U( z{{YvlZxZVIrM=vc>Na)}l>vxSt;b9qiS#_2)1HzhHgRXG__iAhIPI=)Vz)?bCt2E4 z26m?4yKn(LTz38%#qs{5YaQm54Z6u3_fy0?&AS~F_(C!1{wFo*O?$1APD!+gC!YG| zaWprVLMY=1g=HiR3=@?cXBq8OXYnG%r`*}Ye{pj103A@41-Ena9{sqkWhpIQ#*HVU z9~3vbT=q?OCDposBy)9;q^oCP&qey4GHSQQTiGsAZc=7@Z?q&)+$(N?DRA9-79%Qt zm8}rD@8Rf;t6P(6alK(k@TFu0{{Z%781=4$!}IAQL$I@vqg(W~ z**(N$>1Sq@=W}b&$<4! z=@wBc$%O$_DcS(W21gaOM|l{nB9W~E;lHI>i|?Oo{{Tt|V~%`@z(4ZI$NoBNn74?3 z!ad=~9#Dd9X*oY%7p>LWsnjFwm~Af9~VozUb>Y()yoZ= zMsbXfvqUQ4uI=oO9Dt^mrf={etujN%WXJ+u7=qz^qrU1GB0CGytPGa_n zQ%8Yk(3HsGRc4T5*E@Kwm&7`Wz3>jL3zE|%znrDcSY}1t`SNPvWXP#|9KXZ;ui|T^ zd@{a^c%%`>-Aw2F_NsTVp}S;(iy+3*-o$3AMMCF8;jtR{o+W5eW;XX=upF`S4kN(B2@3Ob}tdwPa+ATTSNRzftx?~(W_8h+RjQ_POf`1{J+k(Q!;eg3DM#}&aMV< zSof%R{{Rmp^%U7D4K2?cQyG_xXWpQ#2JjoW6w+`w{OB4YC!7=bQ*trT{uIWB5`PZn zo1F2JNE0buuai7iBsS2%IbPo8t<5H(aT`7K?$)UIjJr10Ur;N8s!8g0Q*EsV>JfSV zd&6l8!4Ql{(I2`2y~lR-$Q~3FkFu}=Wub2+m~q=&m7=VY3wWA-0gfQm-hYH7 zjFfMgv-ew()~aioRm|F4xAzyZ$k9SpHHkK?9B@F#KDi#1%UNror6;DPC$q4U2)y}Y z)11VT2@Fjt%yHBZ1`pRJx*ag+7CL+VnmcB{k~Thbz&>n@WD++Hap)*|#h{Lo@;j^B zd!1e6{nTR8=S^inBK*PM93N~9*PLGJa^HAk!*@I2d+0SwXa&T`Okjp2kGu)#&m*mO z)OUbVwfRdr{l&fhu54oaMxyY$8KPGK5w`#^2`8yM5_(qGg>9~2)X@-&I3ZAEi)+i~ zA50Q3euErV4_Owz6LV6NPL}vje*@0HH<~W(+5Ck#+5!QL6OK)Jzlm>G{t*}~))H;T zsAH#*c5|%UGh4)(5$L zs@8V@-meWZPnWN66&Shmo5@y||`GF(G- zszVQltdaqCmWzd6bE{*`8IR^lnh9U*g%_zA3C z7sJ}Nv*B+K%ct7;ktUZEMnN(z$5(ue923DEs*<^_Jt9wa;VanV^1S^%IROQp?@&U+ z9OEDw<2+(p`+n*slG5df<4<$al0b5P-vM>Eu*Zwwfk(=w~)PqA-A?| z&vb~ussKR*_CC0%ykT<)@LU#RYZkamv}cYP(nGLECqE{6$*ARN+^0Cb46OyNTSpN~ zs@Yh~mk!=t!nxk1RDv)Ma7A9!ue7+VFJ3)T3!AC!1+v3$Zzxh!3>=ULRsCuc)VVDT z4-VbFpQ}8dW4XQiO`O(dD`yKPBpAw)2m}nChv`;rbi277vEco7$X>Eb7SD~1mHTx_d1l503AxP$IFw3J#$?TgS5$Xe+*jK zPmeYL%J?||?kCW8K9vsEvQ{Q?ZQZj@0sf3WpVpNC`LpVC`O-5)NVsV+{{TG?HC!j} zCLR33c;cj_%2CC-;^;FRd4rGm0@YiKFwemL5!>FTMv!Jz)|!oR$m$JJmrrOVPa)i8 zvGW{?ajPhn;+Z6Ro}*#+U0nwoSQ0-Y#aV;IDn#mZ5ijvM8@EE^aNt>_?FS+k$#Gr!~ol zSKR5K^|4qPSM3(*IYn}e`}hX|vHZJ28?Z?)vYF47tPl-j<%1@TuZdpQw#xt}o;7)s*%1+X@Psvg?-DI%cE$$?f+{*AgyBR^5OJ z>42bq6uN{q_xkO=nH;muU1T#9m<@$P;2ex}PX3~~E5_9ktuKLmi~^i54ev9!5CE97ajV=cYZYoAD*w7XB_pfuxcsbx0>jM<|G0S;~$uc>OW&UDX%4 zh!r{mtA?T3s3!GsYr(VELxzaocj3l|$ zP0?$Wv?`uV&ND7a0fGeqvBh%9JENj5*G6@RWMZZ0?T?@TYD61k_Q9(l zcLkkD&QyXk)Kv()IiQ@e(k=75Jv-5MJv$a6(zKpM`>=bEItpkbYm@!C%!f03!NS1MlFe1M!|BdtMoWPG;(54vhKMv6%k=hGDb01ngYD-!Ef zeKz^vK65;07!Nb$&@a%HMHL!Tx{~L%y&vUMRYQM2H=7C)~oA2AGwKGTVGkr z$IBihQ~8noYpyd-W1dvn)Ujh>9-s&Iey3#e9+vVsj#Kmma(GN7R0mpLiXuqr16vk_VPl%wv_1Z3lN94oBo_ zt!8V!gD>>?q0=nxnHngrW|BDfWO4>`^yaH-mp76uX>%gDjTHvoco_rqs*IC%CFp`n z=)TH`w)UuJgn?Y*A@)y@MHJk2YL0VTSv6@!mt13}ANY&rhv-XNBHNt9!V07|=hM7s*^Q ztYs8|kU1Ql^_*VCy<*3BfXNNCmsWF#(%L^O34!J3u6uAc@N3V!X%)LIy|tU!E4AQV z%dz(%APk;*bm9xSBrF*=G3%139jDtZ6ulGksww<~roc8qV`oQAFA9VbcCte)24X1ljZ zG9vkjX&b8&F~G^=C!XE7d37yY#8+}$q`p*4=W~&gPx-}dXwg?&oU(GN(r-jvu+U(R zedQik3-_cs=jqb7Ei~X1;W*EwbKS}6H)OOg+t~$+8*1gfxLC#(t}!xlEctj)N3BXw z`GA31qDSC|85riCyg%I|j{g9KF$)@(cGw9R>?%kjau}j9P{$+#=|G3WZ#%If83@4W zrkf&2pLjBW;NZ|bMqEmYhs>5V;NY}?e=|_YsaQiAyw|pf1~OW882xFa#Pr;PFA(T! z8mntsQn#VLh!Ot)k5snWwwElDd3v^(Fl(v(T>ld4AOZP=_A@|k|{etio#ZGkM?nq_|-$J#S*yw z%JBo`iNMmP8T`PiaP<4omHR)rmF+eAR*pqB8j3P0VlEaqM_!*Za(%wFQePWcMhKF{ zAudin(W%P&@5J@cdx5OHJY%ZOilu$3LZLXnKg$lIl{C zs)>rBjrXenI)mT0O3}i|Uh?1bEgk+aj@Hau-a`9tT|0E@Kro zLq&&u2yUSpRAac~@upl`3^oVwH}a`yw8);&5yICEt=ez#O~P~mM;QMAWK`^F4N7xq zTY+7qjFXy+>{DBUV`&!|9-^FUG-}|iEOXvKFx!X+IRgf|KMi=+T{79>xR&ZM z!G3|kU(&i2X3>kHlQyjN{T}texJy|}c0ajc^kqALF;>N{hk8O=T}f#n=V)OtAMhpr z01DPMQ{2i_dlfG=F;PA~OiuOvrP|+GGa@>)REBsOffgGD{_u?XAk8AV`uf;oBQX$@KKE zA;*?&9)g>iMNJao-p@{!DS(n?bG3;et`DH&^P)zx5H`sqot-hYxB&kET53M^z@P0` zKW5VBU5J(|B0}n!9c^VJ1=46v;;n$Ij4!l>hcm-ykz%lAx zBfa}GOw7n#NADF_gl@d$Na^cJuPUPM5WlrYH7=)Un$u5gBeYkyAS=(xyJVL6iyq`) z9CKKHB=I!O;aD`Q2D66Q;|nrdkKVHYMo(|ky>laHsZd3Lijjp3~>&+Krifu+BW;T?-@W@N_RfMs*vfVI_5$~N+5RTK7!_GsSI zE&^!!WrU8cG*SsMZ~NIFcv1(Jqdfo~{8y#;D%x!~M4wf>o_RN`v4&_&Zi=S>5Kc$R zqk~w{yqaARtn|N`4BC;_bhHpO7H@FIWrkUDB^!=&fzu$2A4>C&6zebHon*rB!t>j* zG>ae(17tA4>6`)i*G;+ImqQ6jQrzVHQSlyIi6(1t_B44NwxJu2<}5)Yo~OC4D(hFc zyHelkf&0$__vyAV4E%U@t?H^lc^&7?NxK(~%&B>B75 z313VWBi^nk4qoHNJ|iwv&kS-YAmS7SPeaZPK;9|wAs~%XR6eLtbNMY_A`7t_#Cq$D zNvhow-Mr#IkgD_Q+N{Sfs_F;na==v}aWOCaMKl*r463JU#L`bU9tyNc@V@aV{~jzYoQ&el`8t zSweBrD`LqWX^c`L!9$c&1O#W#cbBE z43U7usbs+f`_?jpPU~UDci(b9w-k;jh(9X6(5DNN*N^kkx*b05Z6*ARh-A1C!X8_B z9IR?Oj)#zUXSY*RBTMWy@>3n=r7f|BIIm-Cvk6{{y(T~p=g+rJc*7U3ad6L)o8Nxk zGoJqd%AyQzJw>>fK??b2<<3DIR;~S~{8IWg*O!@Y#m@HK)60Ae{sx__t|jqUqjzwz zX{OjopEnJ-^NtAV_}8Ooo;~pW)`bS0dj-Xm8f+*QG<0C#W2QhHkT}gnSo2e94j$~$ z#ZPr_smyPn-OgE5unY%cao?{3sn2I`WY9FPxuu04m*IB+PI2j)nmSn(Q@QVk7%c2& zo-26mq>9+?WAZZ|ykp-Sn(|#sREGZm!a6J|uw2>4Iht1(GAyi4&wPBRpXpjsc8iyj zLaBL4CEXo|h88sN*M{MdR!N_K8!_`VkpcMa2E9uDCznQbvQ5TOyJ;X~G0S!KCZ`VV zp`~kY=5tz&rLCOlCC!}cJaL7yvpA9^IXK*UV4CvZ6+)WchdtboG_p(LExvJY-aHEt z8%rNdWALt-OP6DxmznFaM^W&Pgg@aT77wT1EJ)>APqn#rDmnw6I(E%AUxu0_wsyCI zEBk2WT&o+P5;e56ZdRPK6>ltOuG^c;l!1 z3aT}c+Q$QgJa_5oS@M>qLiP>6u=%(I;+b(`K*?PC;c3~}r7a4xS^R(v?|7pwA!t_EB==&i~<2-YWiz; z%&Q&XX_0>JPI35B_Niz~rV^bh^;>St)WIA;-S|o}v zs@ORkbg8t7)(G)%&dcOXs)dOP%o_v_-D>no#9vXC`b*wLl~PsajRP<$6rJ2>(~7yJ z=$d<3N^fQIWexke9yKYz=venPmD-CQmqb1qxmiuLQcF03YmKEt9Ag7K52>fm3=CjF zJaE5~yPj4gH~S-s9j=(|Yo674XMOf7*k-q!TbpKKKF}1E83P?oNEOGqg7d;LB##^o z6_PwqEFcHl82R@MXE;8+X+>z4amrR(buV~NQJU)0N4K-KNuNz%a9Aro2tCLimFO2< zE0Wtp(yryVEpqL)<`v*8EN#4J*}s)d70TIj_tnhEwU78nE{t;8lrkN{MRG7#oB^D6 z{cFqhd#Npc9O(BE7@B6*H>yYvVB#yDrQydn66*I3fgW4H?4uw9uy8Y5M`N0)pulci@yMkL1l@Xy)Z`qa zhaSR~JP7mOclD+M9Qi}qoPXCtkME~Hi2nczYjArWQ*@hkeL!>u=3o5?)o0>V)M=y3{hF$aY^yFlZJ8qg@ImdR^q9Sc(3z#kDwpfW@HYF|tz{N_F$T?6yIwHN+XMcJx%;o$5=^mSA z(qZo*mU)DD&ju~5Pi<*%nr{wxE;bb^Pv9D&oV8kL zOxopXZgKt}l4)dx6^g>Hh2ueoD#QlHaohPSZx)F8JJT|kR$*iA-kgw6t$Rd(8N z7;}&5S=!H-sK0lVs`8yO>el2y$r_N%=4_pr^aI<}*CnhQd!Gwqxt5X0G%USP6VtubYPQV)CVn2dOef!zbIPHPGspR(hrAxCrpPF)FeK-ME3* zX&ilXQz==tXx50Me)bJ#dVJW{Enj~-ai;g+(NFY>mJ*))&(U&+4?rI2GQZv8P6q(T-u2#b5BsZrNnNTt^J!lvc^9&k5ptjS%$9|O2 zEtm8q+jozb?jP%2Dn}SW@(j~NO(C&>ZXE5;1E0dAlK~_>wi;w^EOsV+Me**8A!H^M5K?jsQKk z4k!U8B%#Ua{xp{d_btKB)BLCq_4|X@99JcICf|!tq2#@zW7J~Z=v$35Q{ob1!lQ3% z_{ZVpn&tc#H~ds$J_{IT=L58D?LQH>{{Utc$9zDG{XWuk`FB~UJmWZb z{{ZXMMSH;(Y4}U$@YSR9{7MP-b|1=<#g`WrT2wPw&e1>GHkMxNgT)mUe8(Y!mRtY<>}gIu@le#Md3uM!5$ab8Qa6!z zA_7Q1FdxdGUoLwOK0LCzf*T#Urx`R#;;@u; zD&)kawYH+tE`04O>Le!vYZV;l@IRel=oi+nrbl6EYde`_FLDfkEUZS+&I!Q5?Mgc{ zS9cU`=*=JY#fF)u#l2#_Gg_+>3}6hhlfmF1?c23**D7Yby1GDct`Pd<=lm+E=`_lc zv>DszdN@BZNtnliLHWYGN5>S9!fB*~t>S~s+Y{S9+5w?;*vWc}7+Iurx@t@4| zM5LS=^F>fxv%is>eWzU=Q$*6`o-rHhcUGAjupBaHx8wA#i%GJ-(yy)|)8t9K?G_1S zd&R+wHuS`0M>qvXuUwPeMzx~S6T@|p%V^d&uxR&p60u0FH7Ouyh{JBe=Z~QuooV=Q z!%{brf48(_43P$S66B@|pfbIQyU{ zVl&C~tUXTHY2F^a(&x95;p4G$9mE!8;!2?{zumT|!GK(iqGZEh9*hvCi+Aj~MhA1fB=Na_%tPNc){JjFsRd2HWwS%k-B@j@_-)$t3X~H6!_CM7Bg#2RP##)AaRMPDPhl-A3S!=T&gO%OSH3> zDrd0zG5L4 zL7)QQ;W_^RJkner+@JSP@}L;teo^$UQuq(}Th_nz=HH*$r8L&#iM8a z{WcopJQ%w?Rc(QS*0Yj5_8;^3Q;DPaPe8^s1oZOGfA67Ohr|T^_MCuZHMT$UE7T?L z1vXyrNXLaOl<|orIrRPGVwvMmKd?^W!rUf4q%{-xi;m`Z!`%M>WZtO*<+@@20FIC8 zS3GJISVCI^bh0l`VMO;TnUShoO9#Rm3yWY3;_3#*z}vrg{fNiuQfOBq??t+Gx;InB z%deKJv53hS7~_#n3vtJ5ofVh-dqI)W1!wqRu@i3OD`0Vs^_8_?*`cbiMbDJ77qz*J zD|-24kJMM6SYAyGv8a+5fXfzf&~d@6qPI-lzi}PUf)Szd+p}GH?<})Sqs!Z|u*B^L zoM7V>#iXwtr0S|bmDG?>b_c2II?*4AdK#@`Goo&M{v@3)Joug_12h4f_){kQnoqCH) z=u3Gvuc+Kz#plTbO%T*1L-P5As3rcS57Mw8yedxbGl9Vay;6$hv6V^6_dT-z08xn9 z>QY}!@JBKTUDhJSOL72HjAU-_+MTFst$(F7@JSz=HkWbchEhlopXh zyjn24HD?v)hoHKRt!*TTGOUtXwBC3uPI1|QBOO8Zr)l%q&9297coihLxQ-Yu;vQL6 z&N2`l$J$^!dzgp%so41cv(k%%8 z07$jHl1UpG1mF%mdRFpDMKQyoC7q_X@UF3c8_0p~t?a^Za!5uer@kn%mDfy`G9+h} z6$EU%0tRM1PhJzYde%{X>SMj6$?qbb9Y0D!(U@eNkqA8q2&Ct!=B3l*^EJ;AW*%w-fyu4eQ6!LJGq~}C$Kgp4Eehj-iU(^P&b`K~DJPr*`c+w*2&8B7q;9q% zVU--W_);hv!+UlV0H?Nd`P0bSgB?3l1dF;BKHbn~YjOBjQyKlrj(U1fDRd(kYlDv9 zQpkQ*2m6MaW^Dp+pqlFb9`LtdbTViE038)G*g7+_jgEO8hw`Q)4>A6L)@DGz>zh8G z<4cev7{?hD#5X^@QIFz3%88H5$6~+EfD4229QXeK8Yce$SI43LR12u&IgDe{xoglr z;cr>9bT&<&Z}e$JR6%HMd_e{L6CnQpr?^1J`?bn=MG5gVPo5GPl#KeJ6hznToz_EK z2HXDtEc5)Umht-tJ-a<5RAv+YjsS&&ocr6=PjPznWlKrLGaX@ zzvt>Blm7s1Kf;ObTz(>t!}L|tFSf8?_a(lZe}zrsy}#MEZ0Bl=mLUHCz-XT3!*e$F z(o2tpQQF)5(nln2nCf=36a4D#lcw9*=+Vz_3~|YEZ5z$UB$WrBr%I@;7?aY*ovv2e z9Dj5H`qgb0$Sp6~;gS+FyL^GS9QEiiS=&}7v@Xc@vTAmAmpAsdLQ^Di$vNQ>kb-~x zWOc4f!diBXsOlF{Yj>guQYL7&K0NjW{JA|c2X9*ES~(pNk2F!$X&T0jXRDo5M&jmq zWoVh_LAT7@s<;Xd8-VNDxs86!>;>NCRx3M2ApYSz{{Tj*jnhq?GTEeANc3sdCX-2N zNdEv`=IS4Og;4e=mA{zLlsurPMkk)4&Jc3dKM@4Q!LKoSa)MNp*8A!rU=cXSWa|6(9ry z82pV_ismSxFeSVsDfyerQ=g&hNhQm464BY7m#m;o3tqXoifLd`X(OU8OpJD5fPagJ zAMh`FgI-6od-ycl=)S`C?Ho}@6DxtY?y<)MC!eijO(z=>R`F~4=x1qn_jYymr%j@>Gi+T{%*(;BOH zbD%wxF-I&Hck#-tE9@pvxC5L3K0!s+nXX&U{{RyK9lMu67tF~WK*8sM&(k#uXdOxVP>G&1^x~2#$pJyo@$4xH$sA*~QWg3dItCX| z5;*9WaLIVOgw#A76$J6BQg`a}Mj z2SDQ=_sFS`o`~%uBm>vC@~Gl$NZyzgnUJbSoO%8q>Szu7!ISI#DWGDr0V3nB@91eI zJdh8e{b`am>-|y2at9RoN!mRzNeyl+Wb9s?0bIrA55>z207$ZPgZ}_t^JDcDQ45;i z5F|+a87Y1vTbLQWhAWovULVC!LR2pJr6HAl5So>HI~(cBbUq)~pI6r|5}2hwzMUz;07I5%g?TJ!WY~ z!@(`oWgZEk4yCscGWa>~!4yw&;qejtK5zJz>Vd!Io!Y4URQ~{lKgTRai)r$bziQj` z%AfEQq_rv!QNA3wOHTz_Pj<&*kc8trmmq$%tuk%4kYHdb;6E`fSh-d47Fl089f&m-iQ&Q&0D*!^o);sdB6L6fE1fW0Tcd@(aheJRYOw>N{4ZnXT*dwY8O&t9plojv+1r06Gsr zT~UqWW1Y*D-ozI&qF%kGu8JN4LjcDF_s?ExDdvU=KhTPp&!Or13Z7dekmWZgc6t?* zuuT|AE-x-Q3{hoNQID#gn5!2+Pv$gn%%#p-bizz~WGEk{aY9XKZ8)Y@mIO<9yv@;& zPtTkga(W)zdt!#3867;RqAv;ipb_SAj&c1lL%EZR?nM@*soH9Hlj-u?#rBC0oUVbC z^PFJhVx_s&FLfExTj3;yKIm_{zeVUMS5ncI(zd9M;^pIv#8nb3$5^F}XkxBy#sa9ML z;Pd(ZLW)i8V;o9d&AyT&4#uu{sH}PxfPiGlaU`cbh#-H3Wsi2I&rgSAd_wvDOsb4n z%buW+e_BY-?uh>YXx8|5`>38qHFDRI^AS(_KVFo#L-!4i#D6hctynEeKLpq5mOHSS0Z}fmO(|7cyA>@xWPkrCe(&+G5OJ=K(`h(bVV#Zvzq1an2(E^X&nuNkFV}0rx3SuSKK~k zvLF0^{{XK`}039_`+^A`DHYr3u4^Ztbv5~e9`s@A`W?vQB zOKwWVKIU97VUBy#Z=rCDlu>I*z86p_5tYZU(4R`Uv!aZ<#-l#9oYyXd^7B0msBdxN zbRo}|GpubQAabC9GlRus$aN@uQ#Gr}vgk>%n7B;y#!q4TeiSGvJxwUolv7qWZY*zX zV@YR`T`e^-83tQ?sWN=r`nD>Lw`+gm4SH+IEN=Xla^yuDa7oWx8pg?M*xk=fUZ+K- zcxG9k5swequG9n0xVCgB9A|Nm4EO2lS4V^_^&3c+Ro3*)9K&uc;*aeJ`#@$RjPv)8 z85HVGG_Lj?D9Jq*Z6@G#8Uv0UeE!E>0x#*KzuSc>JmN_Lp|jGTb6W=rT|qK*nn|aki5x!tE;u+*L^a z<|*=djO9dx4xMpH6Dy=+U9GqFizLe_A&%XhJJzx!XCV}>?`}mW@dmuNC$qd=H6-$- zDhi)7%s|5)`5368)o=Bf*7gsy+@-cvEdxWZD{i8!swmlxWU9!&Pv zXr5f9+A`ekE!U?XoloKIK`*skQsUheAuOtx$T$Un&PnGOr3K0ujbe0qZG`%b$co#_ z41;+xNDyNnbw1=(uNLT9!s&WM$pT9as?GL?j^aE+nKCn$=Z{SEtZ{VRNVOBw$fmIM z6zVM<^|S3`D%R=;GXD7kF~}RaKT4J}wiDMGAbxeG7fNa@^D`(liIkSiX6##lPfnEE zsejS1PhtGUbuG>ZvFAP@LVQuG9titPhd=NmRU|{_N1mguE1{Je4hUJCVDP4v1p+nO z*U*|pY*LO<7Oy;PHxiuu*yt)7oi%Qh@5-!fK|FstnO!qi9S(70U_juJ?Mx?$0APdp zQDRfD{X0gwy|^nGiU^RL%{zpRaoCgIwEP_#T3y~m_y=&1iO}+`iGB!laf_ zC&e8p!5elTB(G43O#CI$BQHky+0p z*Z%<3QfkbXT5H?IBD1?883X~0{e5aBE6V1o7I!OX*2heSb!FVLus<&&BR<~rYE0K~ z`E47jhR;3t{{ZXKZMSqYetXa~@(^p~w z>(eA=tNo4;afv;~DYo0VB^0cIY@${e4U_9n`yf%nM5K0Nf2AvxZo)ORAZ>Xg!8sr? z>rE+Y7&EY9KBkQrCebWbwn_;Qr#%X$KdoAZ7}7iefzt+_!jri&M)<-^s6B-}fsBO= z9G_Z1n5HIY3_t@M^{F?-8OBCGEYWba%9u`8B6q z2znzM?N@)>5+%IQpxg8A$Eo$JAKI~RbZtV~B8`SEj1SVS!Ml*%1`F@2y3N#tQr1ME4)aM@0xl#;p83D$&EVJB}^K3g?9RCgME{j`!xk@j*ngaaeL zQgO})r(BAj)5BV|m7J1l!qQvG5uV=8&7_xcOo`cVyspq^mw%Ox+1|GA}+F zc(QF`;p5UYm$zv=!EvFc?-ObK z3uJ%w>YZCZdHw=edW+&;cE6#C4}^6YH96$eEq}9RZ2sun!zstnSdmv3!K=3oJj@%Z z;}z7B>|rX)YmtA#zpxF%KjOpuX@|hNYJbaZxb<)Ty=b3AjJ>K#?Sb%Kn!FoJ+iONiQ3mex9cs*%|Y+!d1P{G}{Zn8_#H9<`sRYa-sp+|6fiW{Q|c zE^y=PoK&b(lypLs3TnD;Rzf}Aq6W%iEXY*^6!?);S;e`B#M$UM{q1X6e{vwz%R*h>2b!n3^BeqW7s!^x@mPr0keh)1DF%O2QYtb4-zT;RG!&55 znbk_<9P||v-Oh(<#sJ52nrSiUF)nUfcL)gS@^eTouK3>2;D53Y^Ay}{386*($lL_T z06*Oy;ZKWEzJs4UjQSvtB6)fZ3_ z99-Th4nBlrei*FFzk~WB&i9vpYhg1qg0hC+nr5Gf z%=W{2{J_2X794SdgWDBC;@?X*FCn*$rf-|g^U;suNHs8RY$}MLv&R+SF$v=;2n*&O zN2Wj?{{Wp>dz(8}S&g;RFYMomo#ogM*wN zYd`H8eeR&sPa#{!JV)h*Rv>lVk&Mux%F%quX>wl_+3BM4-g~KD3H+GKG+!^4c8rjE z^X-aujK61?T%n3rns-S))kMgQ`M7qE0?=*%dT@v~0{zDDEprk$h#-(uJV-mOu ze*wi!8MI;ynhujJpT#8BitK+A&0o~%_P?q`ylm6a+tWhjrPi5l!G@it+sLB>W||6* zrWjPOte2B=$!gqS1k%7CnPXM#7eaosE8Seuw${}m0p;)$~uaf@& zgZUTisYlF)>*5qHndss5FZO%oeYxxSQ4eVq zX*T_B{{R5A;cIfDQSk#vw?Jjpb!qo?3v5*X0OQSp`BjS_8hBP$-+3OPbI0Ag0!1F+ z00H_@?IyYdg*h&Z(HIvx{IK5{wCxYV7fg=ZCVLHz5cKD9IX~w$HK&byLH3~~#fFP* z&Igf<{;68HP3om@m;68EXF91XM_*U}009Q4;-3`VM&j4RcJ?eAAd6I#WAQo+s5Kp@m3+nt$6hc${dGw=uZg$v{SdvZTDK*>WyCr>su^?(QpeKxllcQw?w~R- z`zD_pNZbO2j@){ZX{wCL!YfpYcalt_Y{uEK#@0k6W8SGtcZ`1K)l_ua6k@bexVcwh ze%k>*z4CUR{!+K}t1xPI(V;B*YJfU5vh)7{j?%olhbnyq{?@P=c_Gp67X#$`EQT5~+ zt4N+m^dO#lA~!{5(Py^iJpNQbi6Z{{&WTU)0iV*IsRfZ`Ep&Ed5?)@mMH9%A zq$}wfzCK$$M6Y6~5A>=xI(6l{m%EQqjZS`NgCq2+Q+)wOBuQ-qi?mNVwt38B`PIlY z+f_oZsj?R2?zRW@&1kMmvLug5u#FG=PLAduFcxHy{{W9s4x6Ske(zYi&q98{5Au?W zwa^I$i=_tK`pn>Z9W(y`w$#7bdO8pWwQvFFAK9lL`ZA@|mtpd*<*2MQw;KkrGY+^G zBR~2bAK7|xInJSVAw56Pf{*CgV*&tiCQq^>z{{S(!GAzO)>ANPPRb$Nie_aPn-Twfd-+^&-odKD5_^VNP z*NxF3jOU&*7n)z}O%C84y0y=A!1X52RU<{D6<%fOvHyqC;NX{Uo`-M7JK=Z{D>N=25YPuti z0v7JoQyrE+@LIdRhMwChqHbega2v)fA z6Ud zN2fb_`(~D_a@1qc)VAHKi z!jw~bSRwd%r^riO>UZ|cIr4{lbCwit+jiJ)r%By9{pwczq-| z^QIXdUzZ_Ley1PGtgnFlKKKQ7Rp$j+fXCA`{ixLzkg4Du6L3>;Es~DEj8JA57C#w%bp1O7O(ne~HCX zsNR+((YCijf1^Oe32s>c=N9Tn{{Y9T)ve~AI84MJ%ROEo57hI`HwLyPHM2DmeI3Xj zV6}DhDYbtP4l3j$M3zSTHiKm}!E)jEV;;EMSvNFnlto=XO_n@I;h2a$;F)py^H5LW zyD6jEu>Fr2KRY1<^QlsoP--7!#ndzli*U&W-j9*@MAOE(=j+8$O*ZN_CgL3#*c0vZLPx-EQ-JIFx2q|m(KUQm>$u{6ZPh_NX}PJQbmVI<6>$IM@4(Q z{{Tv!8Es+z0FOI);%?{9C;C;i`xDG0izMyFCutj@)pAEg3z8~%uFK?cTLWry ztb3PIye-?7{{R|bzPp_WmhMON5<42TA0ssU%EkwpaA%zneub*Yvb0g3?Mtlx0JCq8 z=G9A1fotr}&d%S)u8Ab~8hgBrx=T+WqpX*FhI}e^sb&n)rpK;cGz&!{6lbNxQ{@UBMZ5Q+JkLxKrx(FNtaSt zN@_YCz-`+=iZ|GG8t_=JE%ts4aqTH9!_&Jeo*eECHToOINuC80F+|f#DTl~Mw?X3@p+7wAVJEU(5$|wK<>`4^s z%S+R3A-mCD1=x2OBvPyDNYC=E>0PU~WhWch=kyz!SJSt;U%G>g+aMqgyq%{VYWy0e zl@ou*&ans8(0@v#-L8s!yK^tRv~|l21zk?AvHX5|)?Is43%1iny|z>Q0@V{Aku=>ocR-|(3u{{H26Lz06O3j> zQ<3RaU&NZ43`uKYZ|`v%kUpVX6+XvZ$Mueu`-q|OfQi}}bx#YUu){BrD>gkbx~7xH zS7ouK-DvW+P0M7!f%v^sgi<9`lY0LEUx_~mi8OpS+p zMgac+(5ii!=`YC9&2^9F5903_MQjDug=&#>1%LpLrt?+X##+Q!d2~G?Saf3%C;C!! zrQ}p)ORdR=#4UPM+xBfGy+MXd5Bz$WFOBb(Hd*4(l|bZuk@5ckp;5^-xh+xYa=-i~ z6S@XlT^OPM?w-T{0B5ZGonqK-c`dC~iREI2lpoGY)-jiIWRcV;AZ9f0-;E9PvtzlR}k!#lnO9>q6VZ z9x%8YB#Z2deQt@z@6C4lY;{_aX7y$YK2Crlk&qzHRP_?fw};(vLZt z9m)j+eq`0}_*R-m-LBX8>BB}TI-QBvs~X=8bX?#ztH)yTaWG|~Xvww0Y zPXuVfFc#|RvHkmkPv`|mo($2WZd1)2x4WEFduDAKHqqbkHD$cKx6|fTbdKO1kuGzO zliYuM+qDo0k`E$V?bEOCE+>o(6OT{BwRtj=eM)?(=!e=|S-h__+LR3%s8lOH(bEUL zQBM$PGF+UU8SNc%cwHK^6C;>w)P}_olXbh zc|Bb!@ZD!2%D%HP)**S4ds-U?Wk$0vc$``=MBimTG}|6#po)q=GF8uJSlbm z00^y}$|d%i>oWqllW{m500aChS}zyNakF*rk{_H4U~%=!aYwe4UY~%WP7hXxzle0* z+b=aM=4Si56fS<4sA0eG{KS9AVoYVEWy%t`h;s?uHPDvHqPQgBWKN7wOc+uJ*AE=j#5Bt7-*4Hpw>I3uEz2l)(Cw%XQ~ zGWOae>_6wHk&r*{tW#Ab*j~o}0BDl$R{{ZZ?j}Cz_9(Ih3KiZI*J*1Yx2{la7B&a@(d}w?5~m*G%cj^|5Ybs(!6R@nj1R!QF*HCQ0(J+c z8_FNasry#<1r(WKbQr+I@?9$d$%^GhKMH5|U8;cF^>@eddGV21IK}?}!+D((5Mc+My%l#IuS#%}Ez1YXmG>xRX-DDo~XZn9imJJI`kZmSS z{eE}I{ctJ!LU{?I@Xdlg_%0+@JOZsA2h*B={3E(ruxTcLGd#OIS^abRQOr-X@*Z?O z9^DLr+%^YMDpreV?<~@ zuR<}qpQg$^uVyc1y9f4lf*p-%WEga1O#WkNsNYPtljIWGf&J8nZa)yTy`%0mMm>OO zR%$TQH_Cb{6OYcEmKMlWzS(e`^iTyH<@+L8;kQtst~Cee1bM+QK7yGXRuHyQJJXJG zW(0reK+?E-H)34cv_uxVkq_{f!2I`St2L&G&6B73kLqV*{b;it|K*2WeaT|Ec;WQs?))(P5fY@#Es(7b+ThM#dL zQ}z4Bjjx;!KJ^&?0Im8@Eh z)(s)xyT5}`k-{u?w(-o-voKtbQIqnn22UMv>sWVM2Zyh&*)P0K zUU(1AtGQeddVcJJdi4jrXUdb+oU)}W`=!#+Sjl_$%216qP`;V za<I9gxHh%gsPxDpvQ4(Oa!+xFE@TS9gJAR@(jqvFh?j?e=(C*!5uo?fzJ(rkd*FO}8;ls6Xd~ zNyq9cl3hgRuc|Cs#b}=(ySt17pPmK&d#d_JW{Mg0E5-ov{)nUx$kBKwuye9sKHl96 zTQ%hDeJqJTp`?9Tc@KZ6TVVeH%S%TX`~j(Q*FmQGp^vLu$bbz_NzeBtNl+iB%~3@v zynsBb8iB(Q-vl5Uxoe6nLPgh%V^YpGgF(L zM`E##G$VinXt@6X$GN3zZZ}bMUzo8;ua_QFFuZsGmSoTVg+y}eEKiy(6cOB@{5=<2w`d4+S2=lSNPOTBVD7Mo4k({y_Q{#87W`M&`6iT$6&mLk5t z+lOmOasBCl{zjBv>S>fnVN^f!&f-J;YLfF4YpMmj)t+u3RdL4TnTh>r#_w4|*jFq^ zBsO#Pr^)6f(2?D0pzbiPeSTB?)}(ulMqk~|$rwGD#xwd-n|P-ucPJqL06Kd8jtn%KgUG#cU-@BtJ^a}Z{>?^yLkU9pHvHt*}RATQ( zV!JIa7ke=w^0P|hq# z=dmP@)})SoC+?-Bk&*mNoj*L&UW2szo?+p=F8;!IeJ!k>)>hjj_c1fPJ-Tk=Bx9{q z)wPXlPSox+%X@oy;I)_;8Z}}GDjR^=9T;@STIW%Vc234ptkjn>{D0sO>AoqsxV^TV zY0{){OXe9G-Q?U41ms`=$>#@>DY|!xb(@Iw3rj5luBL^H3)^^sF#tYnu=;m7>+4%8 zO-DvGQk>h?#S5!*_{$= zdzv@zzSkLXAo31#@#Xp`rh zDxcE;RCBK7tSw}|7CK)SXpko2bVotgYiIN#hlFK;eNwv9m6asbd>t!; z{jH^(bOQ08^r_(0ygw2KX{@dRJt2{XJ^EEfaeoU;C55iHi!C<2;dEd5b@u{NNFPo! z^r+6g;b}((K)u33<3CPn`IFfau=P;Ud7ycMdw0}R`>Ym682W9gf3|Gkh<%$-zD3X7 z`xJnG=rknz6zNpIyf)idQ03#3QX#sqwUp=a*;fX+DB1$+ZoaYDzaN^_dpYz*GHa?D z{velmKV$I$9*0kF{wACM00|ST<#gW=pMUJ!bx~@)b=zFKG(F)v&Tw8#E~nvP9#Rcsf~Zh8LzsDhFI0HD)x zV@j9vehf(U8@9s7;o`YJ$EgAQ)YZs!El~#F>=+I|y{WE0M55M0e4o$w60QEO*$Ls> zNA?#=&DJF3cxuoe`v`&m0J2b%?nAQsUx5_=0NaIl@WqJ-*Hixh?R6A!>v_WX zV$3N20H~S&0ONH^5$ee1EG$~N7;g_*ul>YN{{Rs~CbFk1qFMkr0d*7q0LJRMVcdl` z*lL|}CIvKjayo&jAO1H|G`=BxXT#Qxy^r>mKlVyol;2V6TP>oJx|y5sC4~i=D;PHgo7*j)4jOMGn{SSN$3?vPeEQcyj|fb*yy9Xvb(;T z6}!9gxMjH)UgMJY zTCtO7c|E?X71B$p2v%kd3!xDCXE?#`FhS2weMMxu>DQX1k8h@F_L_9DwYeWGts|EO zh+d*qTEL~}OOi;n5$E3w=A$AKdleTHgDaOJ<3yh9Q;{vn% zMJ@iCmIF?>WMqJYzUyiG{niT@ntj>)Y zAz@>jjNp^OBOrJ0R_-<7GzZMpktb%vu)!lb%mz6nlo9Fc(z)*b+x-6k$lsRn+Rx-$ zw%7jE0Sz{jYT?cR!y{uL9ze;#{c0PHVlgt8)#8BX8GvmW`U=uA(^glrYe=FW?He#Z zf8wETa!y=ipQk-P5m89mY;c(q#4Ozo=8>Ow&;{Uq3HsKJz<$o6cenNTgeDMoTI&%M zamX(s6YY#vg`6+S%YGx{IgV`c_#P-~JC%7Y8vKX7&9+HRUg}XF7#F;cz>`$o{>knG zO?7)9#tXH;Cm!8tryhuzMqNt=Jp_>;eMPtOcQ9hd@jud~wQU_v$n@*>kz0TnS&!lv z0sQKmpF~7sZQYFlc1*|1(S%1B#l)so{&eg3?q1O1-5~zyjl$#g%?fQRFq9VQ+|P^c zF`;QBkv9JT6HI@`tjBq3gZGzC29n9I(QX;MqW`Kk1gzC{tkR#B5^CopwPirFv~3ZTy8zBf}8_8>v{2ysNko`QoX~ zy~$M6dKJ#AZh^eHbp(G;Fp>E2)9|W;RFqshzN>W{0A+TLbLcu!vtx2gus4Zkjxy2e zSBGy<=e0ooG`Z#DyV&{a$XL*+%eZ?;b{QXzH8qH@_KA=m_BsCm5l|l->cF@9M4d1O z;wt{FC%Ylmrn(G1JJiqoyv@g8=AnNWYE72=W!i(+61E5HS}Z?CXZ48mL~;1S_kP27 zs5$IXNB;mCvj>T8}VRrd##K4ehjP>^VPp7Q-*EX75q&#mIIaGRB60ZAeUng)zGy6zp6=>R8+8tKwTm%ZT_^@$ zurH<$_*Cz6Z4%~mwmpxR&-J9`VJP!$abufJa0plkJQ7sW%W|t6F$sNe#3qc`S+{vWjMnNDA4`exjSuILJ9LW%{vXviZ^;S4?%mxV~3NyjytzxjYSCg|wl-s+qx-H$}_9@aGQbxEyp(HOT z!)^x|Z&8uX2Q>z*;oVa1DDAZ!LF3e5Ozf6Tzz%){EwAyS2EC zCC;aPsj7U#V`d=T_h%#N#}vyi3*Fr$OBJilG^oW~A1Qlel{q=;de=;OAnu8it0k#c z4+~yJa@Npk_Y)wSZv`mzdN_$(odXLHzMfI>yX7X!iraXUMF5myxHncF?Ogimp;n5p9M6-~w~` znq*hOJKo0-{I@LM|YO}E6;%!TR%QL7PVK@~c>(|m_bG~|QD~f9m6hp5f zHov$YpFvt1+k!HRppM6_GFutY=0z$`sp6{9MEgm0 z7?zU9%VjynL7`StAsfjark;dq$t~=Rg#7;imwLGto^}~L*usO*o@qLWuV*6c7OJ~X z+5utLY@lMKGFsdG&oqR32|)gpo1bFiOnh3MlX6z#MfQ*n=~MpzZRyc%{nW&P$Y#j> zD>rx0+BRBTWIiLdY{+c`;P9?UAEz~H+r+jk{{TLb20a06{{T9b8Loxi9m{|4km;G{ zNQ8TpKjTf4#5P0yMxO+J`%n0qR-a;*D(Vg5C>=oZEy3E&FvNrMsjYlPcCXBUW6^1ST^03) zS{@|2QG)p%;+OssY0*A?nD;cE85FTBYnEE}t?9m61=1V^IBA)}xj5he4gl}?cC5Q;+Tv+0qrR5jNfaRp6dj(6kN&l3$^7RM zvX?YV_L4%5E%iXt1vy-h#Cj8&)00urBZVc>>_gtK%Ja`-5mDIfIVYTZb)hLm+9s}1 zda?fiZqnq{(&kMv)u31&JBU^VRhh8CNgX?n%A|u^hRaR+J)&FN&Q2andsImsc|5o| z{J;HkI#-fobyT92wi&1E7VxI)t6#KjT*T6xl|H;fRruQ_BS4fw<>5(k_z)o8`tF}EHnu|;*#G&>u48~$2lvIo?f&;Hnv&iPEyDc~$qu-tx`=~SpUxP58G z=)`S)<6!dcZ(_!O)y=p1R3F;6pfZcww_tL`<~4D{MSo0(ij&c6_W{0|R1YN5qk6NR zl=r=wS7&L#?UFD}XrQ7mX(ou|mIhT4vFY=1o@zvz+B7>(;5q`h&*W;;F>&n6I%U?6 znIzPsyO~E3M{YtX=bg-QagRY%?==~ww=mhtwh?^z2Rh|vlX2=KlhAKi~W6%Ij1vJ6j zkwk0393GrjR@${%0dN(FON99kDIr<`HmPL;U{Ph%zER5J=`O{HE^D&+a9Ioc!>NUMVVI#NkH zk;}35rOJUx+-~<_g5-N-j^+l=G2r)89m3i;*aG3 zG)d{Yrb{jAOG|9O?=}gkxYSt~(+ivnyJZ8hVkzI*8&Kl*(+8XxDRZP8kFic&H*Rxr z20feq0IgEn*r4HMg#8%*0EG%*;L$JHBTV-!>QsuEkxG;!pwa`mSTVo@y4B-oI_IxL&4FbDpm_RMNc^vzS1&=sJhNTixGy0?E< z^iL(cq5fqI^ufm+zc?~eb(iObfS2epmHwnJazY=Zoe70&f|OKLJb#IMOvES6C7Zi= zq{kv{X?c69sE@O0UNoNY;o0`88*!5p#*TDJv@^zGzIP<c^a7sRp31!qd zYY**^)Ltw{KeKl;Htjwgdy!5S!;;wD=c*;~%O(X{njkESpAZFZk1QI_M(HY&oyJrC z939A1u>QywF_x~MP!YH1)^f>nQ3WXjDG}<<3EOU%eXG!NYaerk9(%G%)`Ea>o~BYv zF)o=*OebGt4d9F2Tc%Vqa>MnrA#_3?eU~b?UP~`Dldc~xxTM{R;)7MB7sb@+vg2y! z6Sp=l0&nL$m@jr0(Gj>*9j$(@6d?_s^o=qjfj-WPPy&AIS9(2e(KO%?0_C+@2Z-q=)3y_JQ^}1<><>+a*R`Ol7^Js)Rby`uv(cCp-?! zm$sa7Ch3sP#V^lLF8dA*_s$w=&$Uu^iX|0%B7Qe7!31sC_$YL@zTU&4If zS@TwK@<=oNS1FxRT;#=(ewDvQm1X^LljvY4%keDohTTRFGJE&^A(#&}7WcS9h{~9( z;=Ff(`BnOyzmHiv1>H`M{G~_}tzkF=l9uL0yH?8GDDFQTmgPM?nmgy5+l;>(NdU+5 zIl=?>k`mllg)A|rN7)E;RKI$HsyNH_+%3UABZYMiPcs`;-9hHKYp<{K#V0k1d~1fi zB*>l)y>>anjMM;L9O#vX!3rg*aoXHV?V`VrDh_Q%NExV{X}tBJD#Z*Es7v9s%N2yV znIvJ;t#T5XrU+}tg^sbe!Qe7(B(e{_&{Bg|e!>_auZ$@7P6jgQqKg07kK`#@GXt#b z7uQ5GpKCaoUIfv2HLt78?3hzx+3SMWBc*>!yWoEWt#YNy5kYz9ZXHw0E!YR%bE9+B zxEc72DL|{_Ol14=`ilmkFIqo(Rt+3OAjdv`V=Bf_U_{;hTwf_h9_R-i_HaVJ%L9SSt2pZ-0s@abkWNf_Wrvq(3U z)jE#lhTQ)9%0u8)mA8j~Gk~e)hMjE^e%fZ3a+9 zz6y5AuKo3zaFW%!*GBv4+dR`}l*GMw;IOT3ePdj5=*h1CV9)4@Cyz?OF)Am$2(u`Y zjWij>phfF1`&exRMlvPdk&y0d7RLX`+-X3QYrdn!Es+d^Xj{8nf|u!ZNJk`X3+9q& z4R^C555{Q-As?A{M+9HhK^Po>7#YWMX>}qlG*+CrjP>FlnoqZ#h%b*|Atb7V@u(45 zb)2lF{DN92Tz{5|eW=!d-wCzxzOQqHmf!6;&RD*K;@!8JGmMAnm9qHdkA&k=X~#C0 zY^m=iGV-*xGDGTgUzxTRjjTV8O7WOBM3Uo3o-b5?gEMo$X1L*MOR?ktl%7wDlb5yu zSGz|LJu%yzsE3Bq5EOqdYO(HV^m>rGL%aj&UxRwUEy0JpMjlc7*J@%x_U4HD{A=7r zy7DXmsTmnq-+7CGAeTpR_AQ{z0w*XpfyKwclRN%tLNlQ-r)JYOOBS^ex6doxF-?VOT8xzZj*cPEN)Cj-aaXHd*j&~Ly8?%!Cz zom5<@(9e}hUv*8I#c?1hJ5jK@PlEUiy?6a7S#oh`q!M4Gw-7EB^{>tGj*q%AdV=~5 z$96Xw0Y@6o$nAt;r7_Hc@g<-1c8jkB0b9-61jIZ@UbU%-#OI1wM?P%9WiCZY0q>75 zwyMbgl0}!WS~;FXI``o-pU(PW9g2DSs}QD6;(Sn?W9~Seo5I{_a+m9Rz5N2R3s{d? zOMJpF`ZU>4r8gcW4x)shJcWNcVF9u%%k%572k{8hSO)U^$?ffbB%t$1!$|zRD}c4NF%M z&jkTKorRsNbx~%a_@Xk?v^EBFf;O+-Z!gJh7f*x@ux*187=zA$DZIP!-h%YMGDscn&C8T{-C`kg2>oSR;9SGpQLuP$m7j z%uX|%GK!Javwify+M&(X_7{5J|rr>;`r0B{)u8KbEWw`RbteF~(-;(EN z2RbfORiGEzcf80i`L<`g6E@X3VB8&7s#KLuyF^Q5g%PD?()%N29#}4-^+p9Yg;{T0 zq!K)*^=3rg2v?l;{jXNXj`f;Gf22f4`nYL3e**`e4%Ft$T>r6E2KAFhI`i}Eve zxreOZGz)2fv*hHWWt>=zj$I?;9^9q9!W_~D)M_0yi=O>TO_x7pm2x(Ujg?V{|Fv+z zeeQr%6+!5ij{R0iLiy+?*LvQnCAcCNubnoxS1}_y1!Q!OF3ODjm8*fV=K}K zDjAC+5%074p!5>6!Oj0e)Q8D`mI42}{qHOA|NARIOu*0oT+rS|NW@x5KwS8_h=A~O z2Vnsldk928+}7S&Tm%BK77`UCCQ$T%``W{O`3-_R?Il>9-K`z%pSgJ2JO0Orw>8|y z!QPu+8E)%g=L~n0V6}0ETYCrnpI_0mhdcT@Nw7YL2nqhjIn|UTSRuk<5cDB!8y9<9 zU;cj|AtoRu_-E;#O~4a%6*UzA2Kx2y_Rk}LMN?T`iPgZ~)(P(6=HcjU@58DFw-sPD zf(Qsg0ROB4VgXMGiAabDo{$iekdr;3pyg(urKYA80&_5POFfsBk$f&8uAu%(S3$*0 zRa`>f%fQUW-qp=jPRB3I*D3Usv#SFJ896yE6|EoxgP?-JAS#(Mw&0~_Pt z?Z2l21||>-8wVE;pMVep0Q|2pbpKz&|1tOkfQbPFVqyWYv9YkwrwK!k0kFuh$=M+C zI25|pxa?k(LQ&}@cp!zwUMjum-(X=I?`V7iY8qNP`llS6T--b&&qc+c;u4BV$||Zb zHFbRhLnC7oQ!`sTdk04+XBQt|KmUNhpx~I;xcJv^5)zRanOWI6Z*%kBmzI@(sHm)} zZfb66ZENr7{PeA_e_#+bG(0jhJ2$_uxU{_TV{3b7cW?ja!Qr2?^NY)?zt=anWN4Ey zv9N$xxc@R412X{q29jZ6vq5mk<#lnby(rj)qVOmc(n}h9@j=3Rzo~4zrwORRB0rw~ z`IpuIHv0dW&C&m#jQ$^+|HtHidH`g27-^X3e}WBwNrnL=!}wO{K$SHI8e5)%Zu>=KzSD?0KHi3=u}2-Df18tH#Kk)`1?bp0jGvz zjE%cZYZcZJ5gu2m5-Wk$Lc7!^tF+?7j6}Xnq~2coYQ7(~?LLM2P(c&sRqp);X{4-> z;wisb3;d2Ph)zPv@@uKbF@JmYIC=Jmxz zN?erCjQn!yBSHf}=x(wUgia>JhaUZ$e2WQ!`+|a{kNyGp7b_b1nrTL=oym>Dr#v?Z>+-j#V3xUPT%JSu;SB#LN_i#X>t? z+nto|DUNwA*oJgO-k2f$t0443|KJ7r6)_j^eJ|1U3lV;iyKkk`oa1hGS5!^Y<{}y% z0>9K1-O`gNqDh2&ZuI6VI@7msv!NBy8!$~{xo?|Awhm~b_(|Et0HlxQu` zzZf4*4Q}mg!(VOH6C{0%jDV;eQpcKm9s2s8C2XeX2!iF7q??5GX85>@<&^&~uH{=4Ii~ z&1RnHLU~Ti&PI`hs)rJ47+he>s%sQ56Wt|`d+pgzDnAo@*-*jazpuohG|TMz+K{JW zB5)Y!`2#IfnC2>UwzaVNkj00N+xefzc}KOS2`zy=gSRPJjoaeGzq&vF2k<6*94lHZ zkM?*y%DROGvYL*#uY^hmUeq@UcLKAqP3&QtRu_+%*tPwhilqWn0lUK|f~w~O>cUY) zEr8b7Tlt1vvr;8w&SEts6H|gc%pv4uiJBduu&&b8BzFl)BhFo-W!jrm&YSt3#7aui zOkd{AVNlTmL*}xS8jqu-r99o`UgzETbCITY>T#hPMHxHf?@s7p;MM*@wEd3oi*D7z zm*Tm(A2t-&9QIG`Fa|xPdCSgAkk8o->@womzWkkf^n>vK6v8^&leDovr}60$RRaM? zzGU%^RHYYA03=mk1{>m)*Ei5he=FEhCM$?&Q9#of@3xAqI(r?(v^!67LJz*DYnKRq zZIMjh$WE8n)oSce%8=^dy~GW89F-$aleam>)LV?o1>lA(g*KASr4XMF% z+eD0GTENEeM&gHUv?A!seIP~X5N|W__&_n|_?(1{%91dA@Dx{4|9Ao7GAa@`(-vbi5%x{FPcA-9}X?A zarhPRlRY(*Lx`Y;Cv=oK1kUI+VA^(0q?-%--T(*QE$u~eypO`B1jIL;y45>$3du+D zS(}M@i$d1ah;XGLH#*YTU$Pu#kMo1!NE3CksluKjnaD@ODmm%sM6Cau&;LEM*$zy? znY+`oLo~8=+%1%HpKGQwVyRnam+E8@tPj4w86DsUcK?X*!Yw16%m0?$c`ZL8>_i7$VR^E7Q^U4?odMF z84M$Vky@l!NG)953rt(&qPMhBNSCW`M|Wd;=Muoc=x)L@5Q{nA)f-n8L20#l-r?(Z zo_?&VpdZi-#z{MaJ`iJ5;grK!g}dC7Lb%|?Kic*|tp+dL^B!GwT{Kil3kW(Tb5dd$ zFDu-!iayb}xAMB?Cs@V39e#o%r<((pV`WU~kme~Wtr~C@)TmTk5eh%kyU`(~%-+ySl$X=3&$`F${pCV}m*5jZR3efK3) zc2)#%tK#PE$%T<1B)cyC$Atb)y1x-8QORV~ofsiLg#FnQNA;c%F{>o4(S|uO!D~%O zJ1{gc@G7g|JOhRGh*@>i_6#Uf>I`k>jo}3eo(BQ>qoPvt3z$2PVHNc!{bt=YL~8JM zc3Y}mp(-6Z4G@%*xhyz9ChIlRj?rR+W!lpxa4+gLIJW=g@o4Hy)h|tf)uzshtst4{ z3NwqUGxeS`_G2izFz7Za{d!T=5y#hwzvt}h^f!07`h7>$p_$f7YI_S=-&GJfhkQ&{ zDxm1z?$$6%uGWMi!lPTl@?V<%Z+=1|U78;A!YEfPt3H(eu0sM1)1)1OMA zpiN$~p&QeVdjliCFXiq;p9;Rarmx#LC=c^h6r?}uoNa~#e>mIn zVG!JC^6J=z_Mn)TPV>H22t_!^S@(Cyk<5t0U(4J~I5Ev~=Gs{m>1ggF*P>2SE)`6R zhHLBE$_@?N3#=-2dp~sdCbuV)HnwQIHx%WZp? zRqiMG+H-A_IaH?vaMtHnC6B25)XqmaQf}-d(3uk!&5B;l$OCYD-y=JZ+ME zY^_Zm3pmTTL*#{CDyrVD5Z13>JJc}R9mO$79!4mAQJ|RF)gx2?+rkzbr-4=VMNOsE z@3x%3PhRCfdzgGu#4lf3UHGe%8Mk@$QZk2FD9G!L=bB4?V!l2arac%po~?&}S(Ps4 zGu8NX^TvQ#u6G-{52Ca*39a9K;jp_uN5qHcYJys`jXsTe*Tc>7nK?L&`<^sXp924Jr|Wp9?xptvQbSWis&W`VBmOMh zSGHSMdM~w>nDsv7I|f?odx=20U~+si@;%a#7q@7EZL>i`2M2ADpk$u=nkfV#Y$NjF z1z7yhdSgv3B!IDrkLxD_<Wd2Tl{e(MIE0X%?zhMuw{d7W2o@V4>!c;U8K!5)ac_ zSM2Mf0Xc+05w1jhlZ5MBc5XZI?qlM|QL`U-8+gglqlw#gdQv2y*hNG{?v%wO&ISQfLx{p z2P($%;B8m5%LO~sGpKY%kL>-%x7S1Sm*b1WG5B5siAvi@g0@A-^Ezz?8LF&A;}3Fv zok?hjD7rcZ@=*E$1cZATx7zsYkjtVaJ{d^MQSz{$l4wiKXVj19>M3qpv}J4KKjm2c z4>D3mN&}<5#;;5e8rr(WD-6uP5yxWW#`4`v)24K9SY4R3p>>!qrpp5(c!%nD&0woR z6aJ?ErONoC^7bTis7{k=Dx zhFyVenq+<8WM$1Jk)CVE?97hvn!G!j?KN-0sv4*7#kRD9Ya-hy5hIS4vi z=lkv)b1QN9QpWkeC>4Jas)2%M4i{*Tf7PmWz37f|JD zEij>2(0N2LpSNF4-*==i<&c@v8VpbzV|XDD+$g$#0GIQy?)a1~)0y4mw!!3?^)iOn z&`B-93Z%s4g`19%{ZFvzy)EVV+v$Q#Bg?XeB$j%UN6b&RMoi!S$Egsi816lML@=uUZ;R)HB5O7{&DXjhSO;>UgXKUwW=()u+Q?|6((Fjv zq*MLmrg#V7Q;_j)Y)Y78ORPPb+u~RJj~r*Twqj=C@}zv~uCY7cNknVCg)m{PfvWEc zj-*wCbkrmhY61ClKw4L(z_E4V(!``C(U4QlD;tc)X7L#{I=^$wIkO)8y>^eT<+#+h zcBO1Ji95*&B4o26iw3)&Gm^;a8cK=tWtgqhR@*58v$D*0S!B6W4L5xbv~EpmoZK}P zP~U{DA~7Fa^W?)^Yk4hI^v4Tl@JYw;o+ic|M52Ar61%N&!WZ-_uF5nzBaD?BADnSa z5XhjL6fNQ7g%2IG{Oqk8?hCJ)qJNj3QKqOef4NkuDcD8$KdYUj4{8@Y4%D0DCg~v1 zQtLTHKnl);H$D-|IF~tNO^Rskw)KHkA&7#|byDVgo!hLC@}Gu_B1gkp(G22*C%Ib7 z?Lr7%#*An=PLH!wv!P@UEZtsr`j09~R#eb^MxV)H{aaQim^7#!?hv$qZx5=S?%MZpTI6? z9^(Px!M8Z?C?IQJJv!2uGw-6YbA7YvU|_m4^zGyDV9-v6sf2q=vTJo%Zg+lyz1 zy@_19$ySTzZK4Vm6O7^I4t=Mz_AD8k*R$Qc@Xc2fn{GvhE0BkI#>a?}c&<7+ZN=Fi zMSCV|vRMx2J)NPpe7fNkaMkToORCHxZzNW3ZmB1OCq}%S8%xiO;<8C&``@^=6q!@$ z2H84A*D&n0l2dc(NnJ+ijhc*ix9oNsE;rPTUe&`kn#~l^!PkD0Zab1Uwon~hxWSu-5F3f z(%RU&*$o||N^BzPfShQD%q}o}h3VO(G5)SP!e^Et`rXw2?=q2(CFwqH?Z(bj?U@TF z3TmGd!sE1wK1=*2-8EKT|26EXJeQ`HrnlB#$4(;L^U51X{`gw&>A=PTQbT0nHo5ho z9}I3grAub;(!+f#5j9Mpx=2-FwWnc_R`qaofE$iKH2DRu-0QpE@PUEI5C|=Klt6=r z{8W5_qnNtK#Wh8xVfbw&sJ6IP2pqe864{e+;}At%s*n`D+sVm!qd%z**emaD4(W5r zjvqYOlbexpQj{eUj^F=?yU_0T!!^Vpq3oOh0VhnId0ou0(#2T~FNd!7Q(aF9z9)+=X?K+v+tH^GwpPP!eg;T_*gfZZ;g7Dq#zzV%vJ z^-cr2rVULr$5JPm{Ac>+2VGBGuTJo#>(5UFjUm`e}7O97AEXNBU*&`gcUMqOC8Q_CFMC3e1-$aZ4TNWe{*s0_oZgI(qN)zqHRLPRV-Q0E0i;pye%RqTK+8XXgXV289wpVmyU&m8Q4HC; z$=&uTdpts6T0H9Ls2S$Jo^t>0u%v*dubhfVpXB?D83R3;-@g2MeR?1_4!_W0t(IsC z`>6Ign!NUy0`)f@V1uBx5&=H69~xG~7{2TZq}byK&}4=hD6)|~!I zcaCqpQ%%oK&uegADg=&uUhI(e960Rm&)FhStDc3F+B}vDe|Z6D6Uue73J~{>L8F;m z86R71BE`+iy|<4=eauFY!vTBv0P87o6|br7l&p4pNi{DK{RWCkhHb1(ZxxjgvP3ru zf8%HKrtWq1ilyxB1Asl(9aF>WLcuLa)G_<6e|0T~n%`8_Kd#9=1z+6>YO8 zZltT_55IUAV|$X@8ZL(EHhpCZdE7*%E5bRmr+-tf^4YK2zCO=!z7c1{gbYny)*GG3 zi}ok`5J-xHiErE-_gJ*g2Xr&8YZafJ@@wiK&l(9J$Z#8)ijPx^Hk!dI8Q5P7qJAi) z2=;U4so+Rn%D!y*l;7?Ij&11lr%@1c$kh7Y6(q+NC>17z{epk3v76n5$mOIOFZ3+1 zcdJHGI)p|IihiV)W*rBr+Ss!4mpgt zog?ys;;wW^sx3)ptvG1JfGphb3v!6mH+|mae&vy%_$UNhbl1C9Y8P5oBScYKfR|Kc zV%^qB<8w+(g-Tk+ zKZ^nTX8vXdqCU;DQjN0I>0wWy{Ecq(@NA&iCe62+;q@;GCfPtG}xfC zT^4BY%5cq7$SV&p@aaVnUvtE#`i>qZtz|7&qnRL1)$?M(Pi+J{1%Kuk39w?DWx;Ro zo;rl}&GKoVu{Zgp7plkw)1pyp3c-Md+5GT)myL#!2W->-SXt=hMXHXPXZM?T`=B@~ z@j)5@k5F);Yg7hM#aHr6DWDngsWzWxrYKsy8{LupI}b4U4c>b}{4H%H!%c@&jtWDz zNPF9oHcP6J<440b?$Jz?z+l9iVj6n>Ej!G{(_L>F{jOWX&oVxK6{{9PanA2mc@NS^ zWX*7fvoP5J1tEF+&v|ni{}73#vv=#Qwn!%Q@Uei&B_m0sgNmI+3zJ9bzRcqm7SW|l zg%JJGu{cO?nXyo+=CG-VX@-KtL7eFWl1p#9if8!C+sw;yXoZnc=AMMDUS=Y1pW55d z9V7QR6a9=;wy}IanD_rFki4mDfAAXAmWId;|L>J_Z+i@l;zi2Z|GhmM~^Da#;x-nQp!^82hL5fTzl*7t1{wTY$vNJKG% z6iIL-k0)fxm|eAIH0(aLuQU!f$~ZV-X4!~Kk=oPkAL)nCHzL>Z+Mn0WCKm^#;Sn^l z`jN;_Zs&#a`4AmHolB4D+V`^`8kE44NWF4`_Yb_%mwzr#RU^H$EIOE`dRR=+doZn5 zB@(D9A&unHQF1YN>KSI-{v$%&J?J9w-i(4Aem;S>PMA3uBo1UGaT&;5^oE<~Ta%0d zR(m(XJcUyyiuxS>Ir7+P5wvI}J7-j%ts5$`;mlxKZn0f&XN6WxwCfG2ZT!&_(P$0!9y`7qZA3?( z_4NO12W9j%UafeQqDgQZ(_XeeD~ z7F#{@Qlf+pQCSn+an~2~{V+HW+otYYLZ~G?7Eac~7%h8L{Mg!7(=L^fQKaR>G;)O? z9{AaVr76p@UT%Bjo~#=m+!K@fw$KHHTW%|2K4A{ZxpojhpxIv5#6*432W#u?#Vi#RZ5$!cuQtaxJH_H!h?&_F5K0fZ>UEQ}bL6Pa;j&*>dP@2eKn@?0tt}&GH zXMvG&1oC$st4x1}nYV;oY_$T)xKFCl4Y^S-`}* zAVsZjRJql4$}`ao4pn>6zjy=wp39QjG9xB4jE3t;GK}y1qwMV5pa_*^ z+5QUW2W6+BxNE)94y&f3f`0(F2kA_2WM*jeUHzfY=$nZA#N8ev4MzwFAaZnz3pq56 zql7K$`B8|N$?f#sp<^Sf^D)i#5V4dGd{GF$OQxTgk}%{8c3S&~LlJ_yuIs3mKBf|Z z%^jWD-%bRX=#bDn6?>!zlo0{~iQhXBcKtM7v~uTWb>+0CY!cxRn$Q(0L$I=Xs#XMl zgTF2%>*4W1EyPH}M{h%sjGy|z<1UmK^l(lrTh@poo}iC*Pt5{9tayvP9kse;7A^`l zP|IuKO8G1Ki1P1(3P3s=qatPgP`$((m3-PCzYbd?=^LJ8oh#$~5}SRdo)=`AuB6A; z(8odd=5j{>*Qkf{-M6-O7jA3&E1amNH)R@hM=d+QziY{wwYC+XHWpS|s+5MEzHNs< z9bF^3<}eA6!BOw}KDrdR{Ec7UnCox+R^I3-Hqs}P*q0*r=w~e4wJ_wLQ7zNzTB$S< zW3y@P)5qNl-ee9S3h}FD64-E6-c;4^zj;e0!=QmddX>O^JmWjY>1NQ=~4 z_Sr;s38ZtugJiQev}4x#uuBn5%sa&^^Wm{z)V-n)&)(_yJ?w1?%nHZn+G_s(>Qrvv z->Iqp_LPj~a%W$Ba!O%T*wy|*!bPXwXZqL|Bb|$Sl$o!Ri+=m3H0m=|gSU_bJ&w&X zRnma*@8i8d-9c!N91U8b_<`rKoG-~8?0;zHpc@tKiQg*~;&@g}$EQN;_MXgfc%`~9 zm=R;~ckMcMXmiz@snC_!vV!e~{fc^Q%wO(g=?2d$lx778$z}cJdL}ueZnrXcBy+== z^T&|p&ZX`pW7Af@F~p^1S2)k>5r)D%AttDWADHJ_U0@8#AEYcBpn|JGS+gZd8h6bb zzvt0n*)R@Bq&b^E60q~5LG{Q8)SbNlB+J)>PwgCmuGU*X1I7yH2e_ZT+heFzMx9(C z!oys6yJ|u-g7$=2JwLBXT>x41<&6ndoPCEBPE6^k_Gx>050zDjnua-s!HFDilSgo1 zgfVl>+ZRWg+Ak(t`=34OKBG4DO*G7E5|TGqjb4BJE$zy`l#|K{*D@%oSQPPtR8+Ze z2(G-od6TEo14LBP$<|r$2iWHF@A1Y=z3cXRSE0Dk{5iKK)xf~X?vttP%+`=1t59|1 zfC_A46PI~u(C3ildEGz@^*<>FTx=dx^@-Dxt8u9B_ZZ%9DXk`w>@x+q*Jwvr)HnSn8j_1JUBEQ$Ua#> zNd!wX4uuPtFHXJPX>S;R^}O)%?Z@Em$^<@a*0n^a!D-fMQz+qx^S*+}%{o2bbIq^Q4^ib7PlH z*n6R#XbJeiMx@Zm>hPbqqA7nqo(@2f=vc&++0RLJEClDLWGRc)QEci_>P`Hcq>n7u zXA2y~cYfoB7MMncHn<8><>+kwg`<8ct2<6FOPaea<3E57UWKSP!3KIG!|beb8o+2Z zAc&Spc7hAz$4LK6%c`jC{iy4i!6UHCPk9wU71IE^rr}@dUxO3)c8Vg-paV~^FUzaR zav-nt!;bL4o;gUF7h5kmTC!k&3NzTh?DOYnSMKBfB%)cBKmAQi8jNBYRC@J|a~e_2qdH8S@h z-(Cd2J1}acdZS)~1ZVr%r8vrGc|v#Y;Lt0PZv#w1%lmU2nJqO`ow{@asQs0ZO(5Ee zFdlbN!lbZQItseogQfvwOWE5ukFU(|)l73{{L|B7d1((Vs2rd_DN~P@W?V$G7ma2J z<0R#V?I(5zJyj$Tb4^<-0pd&~hsVe{Rrb31k@`U{f=Hk+5amhJvHz87=HM^+IM&t7 zMp;gJww?h-%3Ibjf_-TcWntduVpXaZ(WiIxN5^8Ax{)J46lw!Obf-rdw?>GSS})ow z5G<8`D2}r2Qv4_27fu9^827sCRKJE~K0_&^w}vFo6MwloajYk=SjnLmb%h)^CscJF zLLQ)DPaS|k5hPkwL2_ncm)E3wge~emR%dC5b})ixY_8N{*5icJWR$;ZwSA8fN6N8i z>up_C1$kYIt^y8~obpuWQLjCBV5r#GF*-n0_^PR7Q{1>3sP5foy8{5_Mt&(=T^VwsD zSl!gZlJo#?Z_;hlEA_G!kZOpVv%I5gKmd7I{5eri2J5$!?h{=Ua4y&ox|L>;bnz+Z z*|9C|Xcyu~jw>d{gsP|7iMKaQMCko@1GolxpW9XZjQ*}F2zTlIQ!pHtv!0acrnU|s zxPU;&DYyEbGsF~*$V;7ndpDAg?b<~%tAiw#5qETUSL&A>C*l_n!We?GI<5s&(+Brf zeVcX3784dhy3+{8_dO9k;du?JsL;QB{}7vFq(`{hyZh&^X{5Fkqv02ZR8cWW1oKUe znH!{h4I3nP%6bm&B|?pXaek=~M4{jp-`~ppYVY6?r5--4D83Zw4RNzceDz#GeN`9D zY`tTUshwUB@bAE)0Y%FwA3Il4iS6DKxgHc#_}6kEmy1|p_7PuOyHE^gHYhIFG+y|x zmYDA#Ppw{A_TRQ7S{gzAZbR@za(crta9zMz(ahh)Y zQ-nYPV^CEckg^9t7jOZSe;Ui=(!O61CN6_8S4L@XLgdJ&{}p~s4BBc>*vb9&o(SoR zt^1MRzB{CkvL|d3%`k+a<1OKh3(q97Dm1$+5hIN5T2OmZxgnaoCJ6R^EF_h3C~*!= zR;PV2@OjLs41?kRPqd6%)zbu-og!ymWL@iMID`G2`-Rt+pI1sf09DRF*&$;E?pXgP zmC&JsnaqpgQVI(!7c;x0z67spYPpC)r|+wTT@I;G>yam3@xPo1lbZ-VoQ^p_H|qsx z8DUjU&ZUUk8fwc|BMUzYrM~kx<(g3=;={@l8-@-L?@DAcr^?&n7QUF*IMdS(1+Hgr zXbV0$M(`3{Vo@CTUevICupBfB3Vl1c1B(Z{QDn(};Kr9MNarp;Tr^5Mfo=L(i9f6< z+IHfuQU5)%^!_IBBhh3R$(6^}!q!KGxSX7cy}&uuU8g$b_wXO3YOWob4jV07s|_Yg zlV`L=M3{{>yXN|ih9o&rl_{{sl?jCQNNx&#+n2f}Ddalm{*?|N)^zt0&3OL#$GJ&M z$knmY(>Fb*MX^^0`f46KYK$}BB<>uie6}k%n4}L>;4>>Tce<4Id7LKa*HjH`wV_u7 zg3^zS$g*~!IscI03u=sL!egR-C5|_UH;fg7s`#OR8}idhlSO}qEo+B*wbWE29%@mq z&6<+S$$E)dDMcd@D)wR8Hqe|CgLX%#a?Ph8eHglVe)R22w9EKv)ug4`r-xZ_`)Jy+ z2Z?@~d;X%=U~MXJFP3G6Ee85^d7PcQ!C}q}R;M4|(h~g_|lLX3g2ubWq z=?g$YL~?-YnHJoVWr_A1DL8=yLf2Sc=YE4++Qp}H9|PR7J@enx7l15Pftb}LO&GX< z&@%V~X`OZ%A;{{-@uq8BBD~W0NvCPdj zEso$EwH{ngN(&1wh2CiMMrn@YT)Y5cR5!q@g@`w5@09HW$k4lRaGh_EyzIH0It9secvU7k_{(ek@x~ z)i4##jwAB-S{*9sMoxR+GPIF*qUPuuU5+TPPL@F9@==RV<7F+v4c*={#bVlHb8syI zNlF<$2Oi(dFu=@cw*wdCX}{&-Y)Rjo>gimWhK|(~Ylh9l7CiXqp=%{E2}sp(9NP=s z3A@vJ4&&akzxyN(MoSDw)@CL$6l&k8;Jeo~RJDmmBVp?qy(14NF|%h_Be$sqx8q$J z?8Snw$vc)nBWujT57SYLDXmjl?K1W)jXe9FrWjJ}MJ9I_fN&9v(FvsLmg#Yt@aSId z6Dy*V%u5K}5aVqU4;KJmv)zCC5#^Df!ZwFy2HDB1B1en z9g{r;3gE=>N!#ZvVU3zCIWiF}^Cy)u#`j~Cyc%-fJ^Lof2#|yR@`}2(NxBls^wSGd zJ;d93+$~>g>MkEjA5;k@>D;smr_u^AaVP9#fK4vmCx+YWtFeq$%XKobZ||O0hd?rSK?a4?^K0M z%pqv739sNK*zO%l{N80g_9eW^3gMNY!QxrnAKoD%t)s9`GDJjV_bAdrvAl|BibIsga4ESc!>Fh27~0vmcsY3s@pXiaShn(%US@5xyhayhj^#-&WG} z95jrfn;u#aU`lB%d=Oh8ur0(u_veo~hGXjApU2$n z_-u(%c0&7!&eL!$ShOUB^4)4EkKOB0?53L!A9aHAq0oX z4n=Y)Qxv0}ZE;zPip8+B=E9T7kkZv*= zlBVpdMml~0N+HZDMj|hiI2hyId4@r&ySm0_Qe6Si;52_{LN8zB{vPvyXLzfIkL0-| zK2V0%odzni{t5^p5DXwAh{4qo#E2n^n$8vn6TeWuXI#3M?jtqDs{{> zRH;7nRKVG}iA&9ge!iq2$oxB4`!ef{Kjy@(3f=_uUrtx(Y2}xVv&Ur}NQyR^Simq2mK!N2p0@$;0I@S`PMO^<-1?K~ zFMU|+@D_LKa4Ncz2rK30<;Hbgc1OrKv-2NvC7M_8O&pz-xtW56ZCqtF_`u=+Jr5M<*0zaa0-ASb8RF{$o z?iUr=C(vD@p$=jM+zLnH0xGQ?9#|VjzvZelCs0>6c4Dj7^F`F1^!RipB`?;aYQ9YH zYYxY}KfpaLs;k0c%ANI2zVi%~kAP2JlwATRk1c1uO~p%gXYETjCNBQIQERQtdOc30 z&HfPh>d@uwk7kKMH&|pNI=?fw{1mSjL!TGZP;Jl)hLax&(yKXdgd);vJ%a{a&>S*j ztGP+RZC-N3*?U|cQaC+7vVBW6%Zmgd4O1PqBT@eg`alK08T8FV5vAgn>5tj6S7D) z%96hD2phJ!QbGEI>E3}_+M4!p(HU}Me2l~)b zcO;^K3Mc@mr-3F=7_yLZ3b7bJ*YTibJIF09f77J5^CXKLt~W8@4mbq#{*>5n4f-&L z?N*MUF%m(v^PgV5J5Vd?Wlwo`42EAWa*@cpCz&6DaHM%d8?&A{9cnF4%oleTg$cMz zNu%=?-M~^5kW`V6UgtUbQMlY)?Q$5AUr@2Pw8F?lfu95@B^-nK;}PfcamhWY`4Mez)ZK)VnRUZ%Yy;_&{VGgzNV1-=Go#)N z2})$q{ILpYI~_#7m`gKr>Ce7w;XT zJmj2UbL@HwofFW@UR$x+<*=~3(aB=pWb)c+Hj$J@032r@$06hTS5YPG32xK*(U{PU z$9Drb{{R}Xw`Zv7CyFFlNM=_m0YF=?)3?22>Qlx%L87|IrFTp+7!iV)AFmW_G}Vo% zZQ_|(WQ`a&!(~Cp_swBk$7QEPw+cd}Zb%WLMv7FQlwfz-bIHXtC#J&5ExX-1UqF`5 z$b7YxunT9FR^T3^zJ04NTu9SR0vMb#+Id1xG_tgK$Oto@b}mNVPxlcj>wiEeNUcT6lcQKE4bJ#X`&JX2Xm5kQY+N_pr ztqT%CQGj{p^{ryY)s}`2iJiZl};}N4Y^(a(-O#(~N#6(w@jsNj8z!#-(FL z!i)e20|0T_o=+5p&pIi#%HAhZyCYD0V1bVHd&6?2rnK)UVkWlQgefN=lZ@jX2Y*T! zQ`qa7cb{j5X=blGz@(wIrg9kE^Xqui)7<+44xRTVh=;;O+Gmh z*K(YWnV<|!a`p*UdtH(=*37N6L|8UQPBV@VQ;zf)FM>xYcaAO?Bkdv{IV=dzBX=9S zCw*CCgSoN ziD0&XZTmIDDP>?EgS(&}gk)f2frr`M*|n6*Xp+foBvL7YHv2=LyR`9=G0##y_j(5O zEnn&KT3p0!AZL-)$tNs>80rF$3FD&mtCm)ps+9X=%68-ZT%?W}PoO;k?r2(U5{>mX z7xb#)d1ShHqeTKTyNe!3K7;f8s2OUC04Wbj1XeZ{%|D$a0!abNnF)QrO!WLIXSd$# zDk3z=!j}ga??B8AR_*VtuPmC}ySK_2BqdSsF_GVC^zJ$X%|~^ACGPJs@&#S2%Np{^ z1b8_+vB}+mjP&QV0#~`6a%Q&DUPx3hvvrL@I3b&7BxGc82ORa|t#tR%#U_mbwJYVu zVR%`IzyXF1PqhTabuL89G_kVGj$=ZsRP{e8{{TN)up1*dBkMs(%+&QLZLX~>q>Ifr zZLp|pVNM25{q!pr!jmvh;jwbTbKb@1I%Mx>iBysIhiJDH$8~4*p*4G!X*|Mat z6qEOv-Ie1j*n)A>KDC#1appkrAeo~x5xt}=RE5Do$s7~g9Xe3Tlw9Gdx#AX-z=;UC zbG$a#k^6d;2leCfs@4~`a99NicW}zY;p1XR&t}5|%X;(_^|+(evDY^jmzzr3t*Ago z>!~&d&sD)j$m6fIXKR*M4QnOJ&5tBw$!lYR!wNuP03hICk^cbJ9@>!A%;_!KNo?V= zUF{{XmQCz%7z{AaZb3Nr_Y{z!S79@C8BjZ%{{Z!kN?MO)sQE;42R)TZZrJqxm0H?Y zNuFgHWQ2gt$Qi)LIrXY+Xp}sTvB&mDE>K}(i|=!s;0&G*r)ph7((WDT%w=!08Bdu0 zQHMED4)cs;gZc0)6s~HLTC(idmRA=Ngwy7RSsP*OCYvDvm4x?{&tc^7B$uSY`n{ounCpkTKaWoQ2$~+R9vxjt9iNf61#INKl}sa}-&4l7o5XMo(CpI+6Qb*x1LN>v_6V-3D&*K)?|oQB6t zP$qKMEsU!D(VTxOx8cimchx4j1Ia*$w+=wqLhd840A){3b4`SFiL594F_PUOnHgh7 z6I;a?^K;Mvk~7zjZaY?ush7w=*?BTS-oP+k#NZBbgO7SZu0P4Qo>=Zc4)OigR_Vw8 z0A8n{VYmd`-zWnoC#5JLjE=skVvszoFhW&n_|ic+d} z02>5=cASh5Gx+p5%`}8`ntrhn(`_tZd#J7>iZqRq0^!EMLBSa43G3M7JY^uVp660& z?(Qags3IwtR7CqpI32JFQ`DYM1a>-#?0ZkCT51VA@?4!gq#(Hn!BFFGPEJk+HiA7* zeYVsh^I}L*<}#pkCC!3i z%ztQ^T3KOYcQ!s_oPZBJ9zP0RL%CjCQu-F9wF0r5%q38A6@mB2;NrBj^AdP^$VrhH zw~Hrs+zc-y=BuGALTf)IZ3&9(ObPkrR|hSCI^_FQziLPOKbCT`KHfnG?vvl?NCySHl_RTpWoI@SRWFY`PVtwoq$7^Kgx!??7pT#|aK3OC~ zqRR|(NMu~dmplS;B>75!I0Nsn>U;L9daP|F`pVV<)y!`Qc9aF<=Ex(B-9XQOTyUf+ zdNyS0P{le;G&b1#HX2Dh%$$RPp2X*-2dzIqE&^P6Y-HS0GG|zoheln)p1ThNKduxn zVNER#jyIlAtu~szL$W(GQkjf^V54s1p##^C*AG*(mrR`@g`Q~qy}&k0M7aYE*M=nf z93M)ii>In-n^*T233T@@I@>@tsS5d#wzFg9BcbP~C*QMU*L3JD7U10ml&%^zZ@S!M zY$O%w?M>LnpOrh1U23{*><#AJ!EzaiR5CPM0msTpgU=mnG32>zGHcUuDqEwhaoZPX z3_D=%#sDLdI;o{}OVu`K!*?_Wb7?Cq)REb%)(&~Ln2DJjzSI& z7!X5aC!b21{?-Sxk~W>zDOV!^N>``=@-vUdu}PX;7Vj=jtu4ivaFCQY3%G3gRF0v6 zAmrz881~OJT1TnC$!~b>t~2I7!szNa1pf0F+6Nm!=cR0)Q$AgFGqnJ%ZD9U!L^ln% zvBJi_Vgg9c2ORb9TFB@%3r#xODEC-7jmxjBZH6EL)G6cX=qdHNN&BY#tb1Wl(87cq z;x)kf;~W}g%-0bxFQ_H220Ms#U^{X$KLb|Y#wWJ8 zf%hq;Dm#8v46xY8wHrx`?Cj-IWKq3NGEO*P z2RXs*+Lobqkxuy_pTyc-oJ`0$l|vycgb8^Y!NABEA5UuEp5l90{$(<(ahGW}ADE7d zfz#JM#+dHxa~E>OFN%bX<)kkvDPq6}Z?mx+o;ltZk=Rx{nNsRxoyVITtd2k(iw)ho z0r+$j(LSe0*6kLHrQfB-*kXb-R0u;h@Tk8w@05UXoOdVax?!6B?($1{1*E$XoRVB% zyo3TWIqS}QQn)qdT)&3nPPCFp-sb8_3WY@u`&fS#GH|_k$EOCet%}>cH!vSI<6)VV z5=??mC>IBe5;|~Ba5GB7X>N_3KIYC%DMXM)kq0XjOhGP3rg4%^dQ*hI+2wAp7$hY5 zYFrj|TxaGZ+#kTx@&&1-Z*vvQLP#7s9{nn61{6>rqJf!{HnQPN5m`shPWc(Q^!vZo zpQ1+5>E2rnrDm955Bg}`yHY|X)C0AR!p1V~69pYIaB*3l9>0T8(<8TzGy8~R4V(~1 zAbyLUeP|o9NSb{zD0G{vWNBPE^A6%Ok&mFq6v=IFl1oOjg<$f@B_C)^kQbu!(>VVC z3ShevPjNiUEto45=G}`(8;ID90y_7{;aur5#o|Y|iA>iLF=t`la)(l+oN!PN!xURl zJ!0+}jqFcpExJzfeYMP_720Dc$&u5Xj^mCv>rlaPBZT>xSKEbbbu}y>LNNBQ>Y0*f zNIr5(1`Kw(u=fOo{Eu3e;nr!SXp>}OETmzJ6u~6qp1ERrds2Oe9UHk;+7B`-tlh^G zAC(~6^G(4$&m7~{rO*O6{q?Y#Ga|$iL=F|)gS!WgHuLrT1B$aEZ{5F{+CgTh2yLvq zfPfvKeKJOU>zmi~LlU*hUP?eBM}^K+R3DdM9X96}_opV!Ad=9?p6YA7i42kvftJbJ z>+jn&U7>l&kz$S+BVqTILl)`4Vl&#KeazH(K^5d#RLkWKgF8ViPZ=A%De)IXb#;#t zpjO^D9-xdKy|Gf&gsk@sv4_lO%PcT*Hyr1j9;1OxPxU!ml70UG&!MV-Htaa4`yy;l zn|28;*Ch7;0DzCyqqhOX#DkTQPac?TkINL^%($yFTF%*I)1B?)l3k$gOds4Hps3x- zoSuIkJ4tR{S=JkQkpq`iEP<8EfZVP*JQLETTSE11o3cVL&h zqK#&_RajA=Pn0e)2JgDRc;FH#y%>}CQZ!BFy2})9`yO0~!zmlFkMR{gGx$&iZ9|I- zj1G}8A6#a+uL~lbX{5>Cw(O*H{pCOB>q(9~v#0w+XyP!f?S(kU9E13ZuxXNTQ^XcR zp_5`TUCeP^KZV#yqD3YKS>(<;;n;sl9^-=j+BNY_cUY00M+&RSW;g&)1z9yq7~?M2 zmiSSU1A>15I-hEVb=@jJF{xUqE1qRgN7NpN*w7az5yf>JN6qtd1ZAbQ^f&NMK)T%Tx3*XKa^EmNFFn6nzjJSLCXL}~ zU1C*Gf>J!fN!^p|IsGU?wX`kT#pE-_u`;H2Z!#9#dUKw0fGUZE&wXK$8zS7x zvDpn!NDh-@H$oB4%-=R?oc>ZlLk%elFB4-1~brw zKU~ppvpRfjKr@q!WQwVHMZ8feOwcO)yCOix9k943BiK?IkD=+dFkDLl&kXkwM2ok{ zP*q1vo{V`Mk4oIRp58Koc_Ki^ekUPf4CfrfgGk^{;fIE(VIudoavx-Y_UO6s4ZR%8w?t2^_tSA|JPE>P` zrEtC*mTNb*NLmNU&*5IC{+cGgxxb2OAIXJ;9%7sqd;>zwfwq;cI_&kM8@1TmvW@|76?ayxw8 zDQr90GCfm9zLIaWvqkoJ}3o&`A*WOr_^)rR%g6*HuTMII3im+ zsiuE9*et$kv4|c9;$I`w5;_sqh%PxTb86osEK;q`Bw($}oOJ`IsOP;V!WT-#Yb`d} zZzfx?zhzG{F*K~aV}Zk*=dUFC~%Bw!lx-tfT5lT@=g(lJvn3(yEyjyJ> zwy<3AMsj|YPkCu~AXSVVp_xcHtjo5f;wzT6`)8I`0O2?9#v42gaC`b=DRssrOE zJunY`oDO@|&YP$~V{IL|j!DcCNZw_p*NiD<0{}4~0D5)fzj<=hsA&|9gMX&YZ57+x z+n9?Dk~;;%b;msq9X+a)`tg%;SjJA%{&;l3w#7@`TJT#o?Ng`JK3+fz+ao;p=9Ak(q1No^E$-~0V{RVa*_DoB+^T(! zJ?i7StYyjhK+hBlp~74e*A0L+XP?HpUkHQ$00`BYZmt>6;>7;|S{~zul+m+PHN&?? zyE!8fo|G-b_BnYR2}>e{ImjbF<4k#-8HgmFPge9N9cX1Ep|N||G`$`>XhhMNV_BGx zHjFPkk=T>a^s4$aVkKKyV4bbTMXSo3<1R_v+aM|J*MKM??nQT~-rU`reL4vwdrN=> zraj8T%jYEE1M~6EIQJRWjb*uiwE0up$iF9(GYzMeJHAqVJ$+~d*?y5h+POI(a(!qh zB%UONp0oCjS7B$-^^b+{wK9{_(l~RY;Sl zu(-5_*GcLbEI_9mWjcfMuM0;8_jn-hG7CV^p#xuh6fPKv% zxQAACiKU8rYr(t*5K1=2F~>Zdo=-lM+k0yrR%VvsTWg8Hb&@#b4~5SPNc0%==8_Jj ztL+0!y|!CO5ouJ!gpYvg9n4sw4v6-cWnjO3cFR-MakiSz#|#; z=cg1C@RwUtvsB!-F+O?(a7p@pHOuQdit19u*XJa3m68SA7WriGQJPdX8+&%Jcf(**O zj^%E)OB3&O^A0k{X(VLeciL&ClsG`Z6(EofNgwAmFMUdui$dE+7>5mGcX0mzz+jJl z-1lxX`Fc|{`6Hf7xZ;(>USxtW(s^Z620t((Kf~V`sRV$R5AUX7SSH`*66pm<5tNfjQmtuLIK@VB?;jT5L)5W-hs@-OeI6F~w~XhER%0 z9W&@N&~uLFv)NzlX*p=a0s%wIXFQ+&wnx&UTac36sT49vxWeesf}!wn*WB~#>snLk z(nF@($W**DyRl%o5#Ry_Pp%Jc-Azi=*=U85S!B`L7?Gs2kgB_cEwPo9{qCHW#wrOj zm2yiW;07bMK>l@V_93r$FOQcaL~;s}8A<3s#XC-lNg;nOH)P#{!AZp@@Rb#E`bTpl z(amoZi0Ih!GlT^9B#czgSsXg~kZzde$xDYP!=2N2h*QUb8bpInMFI_pq@p- zypZ0MishnZ-ekzj4hbXcRCLW*^r_~7?iOfMI}r(NH%zZM>7Pm!n$WcD+0esv9qc}R zliV1P?O@{@T=IUs2jxL*kQfk8Zq;kZmNMg;Sq3_7Cy#okrzn!-BOxFoICVHGK*&Db zDT*e%clw8rE#|of5s|niWM#(ywsGICJ9nYyxE2zHzLg?X18XR52@8yo*P0P7?8ssP zQoyo+FfrHs@mhLC&D1({QAc?yA_A&`5P$(AsTrhJwl4j?@DKR?m+^@|!lIVydE+tM zT(ps#Wi}|z4o7VDsv%#w78S9Glw+n=kpgup2pz)mDHrH0Q{1JxTP5tP0w_g5s4$>l z4DJ5_IHa8U`-JmNDlyNN@)AER;+ZymM?-US2@3_xkbp_swOM(`W7DVVeQMh3x8sjC z6YkL*{)Ux@u=A^54v%bq?VEq`R2^FX031nZ54&eFEyQfHr`#ToGMQ* z0QTP4{*=aRKB;LKvWjU~lN?cyzjPdPlhAdgGHuif4dUThg9zP_V>?GGMsP<=`_gQd zi&45;TZrPejKbxE%WW^0DGa{keAxixo`S9D(C8Z>FhqiOL7%h_-Z41qo~`uvrDCt# z-&Ivmsse$~0g74xt}S9!V5_$-9QWiNwP!Q{Q9uP00he=h;R@MojZ0;nl;_uh`Eyky zKwV{X=8S&x9AsxFkD)a70;H@iWHF!-tZ2k>jNlXfD<@1<5_zl%mMM_O?gu1(c>JnC z>|D72079|1oz79Fbd&xB20yJ@xe?plB=Rz+>GY^#D_dUn36%TUv(0jtuLu+pdSmc%lV8q~V$s@1( zIQ*-j()FudRHd};u<8LZn28|q$y1N^PY0e4)|!0`?aJRLVPhOu&W}k$-hH|7LIi+;~(&3&bJccP7DIK>c1pfe@YfZWlYpa+Svd>|t%Jxz^LfprLkQDy_2^@|q zLrm0`^?a#-S-{8`J6Lhk-nK$oqWp!8r0S({uy8=^!1`4UIIG@*0UKjrq#WlX)AOeG zCMK3fI+69L?zW2=TRqTY>gqH2P$p+Qh2x8M@ELa>#PP*w*fN_q$`Z;~gF#&h$s`OFb5xvFH9W$aX`f-cmQ)iB=pH2N(4K07YCE|q}&Jt)8C~r zlNxeF2rt+}dto_kzEIEI>>o9gA~}`i`LOO=r(E+$ zWyoTJ>20DZbZy#I^FBaP8;#5yV`=Y$n!Ove+K{>~MagtX(ZJ7B&m4EbH9CQveW;x} z-6XR}OU{CM6@w7t4TFsT0J0ArwWilLzHFA}ctZtKvRMWWTujs6D-Gfb!v9U zbtumzWMd{z{0ouvt3TS>fE-C`PrNKY_G)$nAk}p<)$Kp;(|_@GFZQ+Sk-2S{WBsB2 zG`W1oaqXcuD3yJF(f2NfUra%yyoKgZ$_lAU=As zuax6*vaiZ{CxiMLsinmby1$jU5Xccer*Y?xYC`B;hIsC$HZmiU#T|E|>;Rl_bDZA4(H1^7m zag~6VKt9aJJII z+a-D8ibN=@gXO+{VU675lZ<{fA2vFY63(u!`<-D5Rw2T=?mQp+b=Ldt52Tp2e zBHD^sS*2l+eGOsMt7uqJG;;p?=4_3scP`?ZeDzLyzmIs@$Gi7Gu_}^BT=>6f53xNgxiH9rNo}B)hYg zFDl;h31pGfDkkoE_Y{RFnU29@R$Q?d#(3aW`zwn$En`D{Z5x0$D=|_&qZ9(Yiyv;V zE<5`3cB4J9pK1VHjEvxW15WcZ zpO*vuo|Fs=@H%v+?#CdH%uodz`49HKn%D;jhl7velOM{azSLl_ZzLz1a2J$31@t`i zrUp=+Ym}aAfs$AN6+j9x)AXWA35f_|zzlce{N|Q}(!=!UWxfGqWQ_x}j(G)f>yGty z=4Fpi(Js{-#Vn~Om@|M2V+Zrk;YEa<%CcDrwA(%5!HOYn#1+BGW(PUvf<`^4jM?2> z-rZchDH(xd9%%jIGQ<=z4w>8sdWP9nEyt(`e2a5-}c_>2}F??lJf?ml6k<@#2Im?@${gzzOQK7mFKKNI;-$x6<}S+1TNhKxfD zfMD)91mpwj?@JxSTt^^yHiR3vJ^d)KH)mnf-1#=}u|t=Umuip>+zPR0G;IV^2-Ek7 zPS;g`PzDI=ky9j6df2ZaR!jh@M@})04NDc`BCL`i zVf;Wa9S1?v@#|DIGjOw7++42lG(~{(_p=Ov&s=u@06bMPIdmNyzI1AQwglw$4S|9O zUi~R@sfj9CPc~_pLOAZW-r+ z;C-W&Nd`tg+2M!fQ(~tV6jqtRB)M(N>&8C{X$T}&Dii^n^uWi`rcl15#f-b%=f-@{ z#7B$*eNW}uqqSAIyWWS(Rc2sDH~|~e>soCQM_Cp12i-G1HvFM*JMv8~p>7R@jv!eM z8!}*LvB5p~Jbh`ckX_iKBS9Y0slYuk&ra18kj4Nz02#+YL7}9?;fgX5w3E6#;DMeq z)1^|m)FiuxEx~ghL3A5SDdz_ran3%bi*+HrO-R|H(Brs_$Q#LGh1+lp2wpuo{*{J6 z+yXY`7{*r}$8OYg68MeK#7e|)bJU-DqYbhJ1(7g&5IuWwNYkk-a7V!iqXTf@b^79_ zhyg0^%PWnfdXE19=iZtoPUQ>OUf$wpz}*~jutp?w&-hbx>BrmQBRiDkc*!TVP}4NI zc~E6na~y3L{4hS1M|9T{NU^XVC}zk#2=}4vOi4V6BXwDdICNs2`53EE>(((r3{u+M z?}{XhvdEZisuw);=|I^y@gABF7WZHG+8^XaDDeiBo*}-@bI6SU08v3UVg0|Q^z!es z)BRj{{{YacrN+5D;*ugx7RR9IgMOMBMlhf0C?_y zylRcz?6*L)s|!ikg~6a!Hg2Mh2}Z?VEpke)8SRtLJ*u7HXl~|&kir*a;9ziZ_|i-V zrQ|TC!={O)2!D83;P(FTsV=XfNaAy+s1OM7#fu2naQ^^rkES@F3tElzZ6t9^X!kKJ zNq}Sxm{MV3Y^vG9v2sQU2c6!=i`!ta9Mg?8L*fyP ze`X`|)9obZ{{S7$LvyZQT!d3Nv_<=|*{MVLw=Mjr1gta5xtdWwl4O+Q_tPAnnB<;K zOqzU;A#0iN$Bn2kG5CLtCRNL+YZO0rBs_H}Dht@l%e7pn3J)EP0bv<(gXag|^!1_G zZ*b%iLD|M=1$*dJZQToZ^s9EvJfJz_(BhNWPjZ#(uiDrXju#*I$fdJ6dA6{^!cIMY zWgpg`LS?xf%c}^`oRxAwJ-Mq|dl6+A44^Ja$mj2WpU#J{{?Pl#WJrl+T(|{6Y~Xw1 zlG-8mR+OS(4xD8B+OviJgQC!R>uPwAJU=JY`l#!HU7_v6BXGGQ2{Qc zPjGqXgZ}{5s=lETwYA7PeVQa``^xIN*U)8r*K(v=&jpVaWP=Q zoSL->ucyZ#Y`T;MDvaf8V%oc>s>m15JNJpS+x)|WtE z5xPcYa8L0Q_~xNiWC}q&X&QAVVZjF^sO?c8DLhZe!vl{|pZ>ixOWe=YrvCsa(1MQ0EijmR&%Y)RL!^!`GJu;aNV zs;;SK=oA%^!5@I8!gg6Hj|G&BmP~`3^~NYdZp3SOAY<~|k%7mQNA;&X@`X}<&a2P- zvL`%!Y3MF%iX{?7Fkuql;9)WTH3f~f`4qa7w-iA_Uk%)oFQSE{$zEBv#N4G!3 zGg3Q>wxffxGoCmN{VC?tU@T%#rz0g>`Qn5!oyx3opil-m8+-o%oC--`KnDxg)0(b? zmbDR>)bW$a{VEIntEpU!kUZ8LeN7}A0=60kP!8zXPDlGxUGZgc7BZ3#oA9K@T%J)Z zp};71fCr}^{d!A5{)-!Pl3XAA_|za9bsEZJ4;r{t+sNtOp%$@);@L6%;y?P-YCln? z)(-HfBhZiVpxZ<-`;^B_^vyf5Sry&{10o~?wlHdKIaVH3R3CBv6bYk10(d3xdt_3{ zEHI+)ZG$|%(pQ1VBybNSsT2YxRn7;Nx^okhD3BkdrpYO2R z20D7s1TI0(1XR!iv<&V8830kxI+X5|z25-j;19@AY^FKb9ycQU()@_R57&u>&Rc^!hhnQPzZ;dWjj9 zGA8JJ#oUT=+jEjm?gwG{dVAKj!rQ%y$Y77nP>toG!2^?>&ATThK^Qp49<_Q9ljgJ7 zy{SzU_ZH7>BFP!IVywNvVc*k(Rk_9qApZb5rp;-qM09a0NQ!jSeN}o><221fH}#=Da{# ziF=zc{Mrj~kS&XHdu}dG+_EF8zZbHad{GsWiCd zIW30h4+8?BWMk!w;Ym2!d*hmY1=NRCkIcT9mRxW3=rdIG_Efvv%at5pbGsx8WTQq= zmI1(0dV|JzB9_^A-QQ2QXp{!|hpD1VK#5}QacZQIkue01$NXx$bM+uFu!RRRg4x-F$Vt^b6b}lo5!{xyGb*fgj{#=o~K_bT8 zsDi2m)8hKuFYXxr^@0x#JH5SVOuOWZpg=M zY{yQN33n;n*^4u}Da%660;4X{nLG@GgZWl;``TRXQ^%qHbtZ!d-oX5)o}QG`k2L!) ztuYYDJu!-!5X2m9IUbp!!?=xXjV9xsFgQ3AG%>4hActS}vp{Y&V2Wt6-Fe6rVhF@Z zu>jgRJC7%t2^lL7DqJ(}CmirPf;)RsK&R}e2+3Xn&u+NILRt-%Y$f|W^}W+8ZG^Kp zRr$7*VaMWB{uMWfFYT@^WtQ4ic(985SQ4bXY@;0pF!>(8jw$vPHSaT?w==XBO3sB1 z%%Mog!68rn)UA|o(rsdPs&4ze2wz%K64jH!aSSQDh|^;6j1ILZJwN*N(Yr+UtG?u5uEx%Fn<6lacR&c>0=s44&@Yg*oz+AUMY$E^>2m1-$v7a9P@4!w`tY%W%DH#~yQ>o_{J?_P+3^D!+7t+duxg1W9?f%0bUU4OhG%-puD1 zFOaRr9jI3s0;K#jB?bp4Amf^UlXd0bvN$6Ll@!>u5Zlg4D!}A*X6O3V=msD~JP@ESt09TsbWODdBh~%6M@kRn7s*EmkgV+4?K&&GXj=Y0QYE&F& zoadTg&5I3QeLC0d>tdI1nKvl&b`{)t9S(UU{xx3vQ)py2vO32slZ@>-;{jN7zm0g7++>&Ea7Knu_gg!9;SsH9m{l$KCP;~Cz)pcNrM%O?6~{-e(v*ka?hEA;*$N;11)) zNT~qboz4dxxio|<+a4kU1uGfJ&Px%_YCA4%0bzy=dFzU41c`v!xhHl{@ek!msGG*q z3^U5D3`&~>ilR-bNe8b!ojcUXA!Q<+jDBXEtGg^Fa(eNVOdfuprCtPpz>q;6zQ_90 zdx>n=T78!Xq%;ILK4}70u}>B+Y~bzn79T`P06J zUupUlJmVBfpmN;uJ6IKc+2-?&TTkbY!F7zd5Y z4nJBAE;JU$1GY)1047x)^npD~VU9MO@Ov6p+TC2^Ju}jnB(7PFlv^r%yKul9!0Dh6*}kr`JFI zda}3aXC&^B?|D%{SAaN9<7wVG=dL|HD3ciZLM?@g#&;~gH!&(QDa!IjbN6Ypg)>e@ z)rTW#>&6Hi@t)Nydy?LkGs#@H%n9c>p%NBQ5=SF$eq_c^rCA8E7)a4$f030zB!8cA zPaEaEf<_EH*??I#;AaF74}530ts_#$nBj)a(qMhj{y3({(r+MbRzE2IF2{_F;M6uq z@XlwD2&4Tlr7&*6CLY}ZB!W5m z`c!v{qUDz)V1b{)nhWSlWXvFP8-vK)bCc=PrDDpt$`#MQ_)x;&6yW^p^ymC3SksQ#U4 zi$2K!QIbzQ^b`|wRYI}jC3}q3T}~L=OOOU&A}V7HbGIZ0{&Wsy zksi~=GluI>TSg)X$NpNMnlf?xxogk>8QVBlbn+-ew+$+&`)x}UG0Ow zAcOf-dV3bNZ$*B&_N0n?B2s=(>gO5gO+NhjmeOsFR0v4*HDAmbA9#Vs2Ndom?8s)5a9|a(M*|tEO70aS+{(H8 zr-dizY26ZL>zdGaL1g3)b*y2!DTSnDMs2=+a#Z1 zj=x%8w?~b}LdT$927L`7Tv2l~J_7(dWDa=xdr)V&>4=Z7-u{A+?kAZzIsM9zqyib& zAoUyyMluX-TQ7X?qQF5t!Hn$P7zA^-AbzzAO3cj2aL0_C@j(rMd03srxgAX=(hlO_ z=hK?425j25nGkixHjbm(n{->s@`iBZoOkb1QhF2?`O5%YuqUQT{(8ZG)AxOzW^LPxj0Xqa zn4ajctTzFYGJA7X>~}@xM8lAB!#xP%kxycM#|*2}2ZF@^05eiV$`=F@83-Vb2NbMJ zdZLDsRUTz5Fz7kQqYBZKF;BmQ3O15(pmpn0eM4Kj0dT!nf^pycX^}p~0iz5(RP$8L z5dHABF&v{k0L4PkMTTJvfQ+2xf*D<(g)FUr4_3Lfgzt|)Is?ow_}cW&JXFDUn5cqRjd^H9m7BR7d2W2yI7jndcvu73r8W78*z>@ zDx|Z#PTQ>R5ChNd`cNmM5A6$dBXi1x_9>6UAk))Swb`_n8T4`dvqi2K9@1GAMqvujescJ1k#?so!uHpMmVIhc+aKN6- zFn9~o)~`XW=p2X)(R)d3tO)-AbxuD{)Fq(YQ`E8cogzlt1^h7%n+m*G643cx2B(+3Swg~*JPZ2Cd_bh=y^ccoL>D#>|)-pn<{{U*SxrpIbOKEpuv62V_KD_-Zlet&WNi3y~ z7jH5lh2EbqZT_HOo_(qb0Y=*6IO+l0O;|5-TYEeCVG>$ePckt$A|toA@ptG&DUNuL zl`Kk<4$Xo^Cd5Qzmw4S8qNzRU+Zc?FN8?K3=0gM%D*(u+JOjHF!5pmQ2*APY+5ITE za|+O0C*5GBMmmv4kvr}T7v()npWI9(vfYPnJ(Pzt2l5vB+i;XtM zyh?~Am5$`;^JfR2QT=Jc@6TcKsUT-LITQ=2pEPS7!=!DHtZ=|$noh_017n`QN@E-* zWL>I2IL3MP#cO%2wvlC!F<-n-98zc_qbvQQHEc*a4oqi*$F4C_m-2??0zi1cI3w#z zVU39Sw@@Ro1jwNobByj4J0vh&h=OKjd}N&RPDk^pyJA;tf@f&iRdxX829T#xlZ>dSrq)t89^t%YDo)2c_!rQdw1_dZUaTq#=wYDMJhoOZ{wf# zNHwc$wpUj2<`<51jf#15KEZ?}XXf<>tsbGOk}&|*Kq>4>c>|FlIM?q~jB(z*#LSm(m*w@j#K!O0^VuyMy>RxKyC zj@%oF@1&JvVuIa*pj`2sob}C9A{wS=i+hX$(8xd;Yz*V|ts5_~yZ}s>i?olJJB7hM zfDV5emeFvImbsuT{60rImHM$Cb&Dup%zuqP`#VLzf9tCsjyo{{Z9K;cj-vyL!s958}Yc z106jLYYjh1eZTQDm7Dm1xP%or^u|R~ND^&adDE;S7&4in?$2@+zwyJ!{4-M}wvvnz zC1z}W`K5PH#BTnTH-gx9Y`Jr7Vr^NoYLKvDyddCY=Lerp(!A}TeWKyA#wF?Nx2df! zp)ppPJFOXQ+fA~GC03WnOrhtUgPxz(t9Y(9T_(kH>P`G=S%xqQus?@2D%TwLIg(Xm zLUE2acjLeFt^WWH*~vDCX*qd3_6@sqc|nID4l(KPO{<9~YnS1JQ4-+1DhC9|E0RCp zK3^Z5T6=5IN;amC8OByxIS$};F}=f?@rVZ?~dv0WFCM>yPb)O5#MU-(DmM%~R$LcdaTt3_arDX>T7{D84W`>#N{x4@>TkVsvekE$ z^bB$OVyjJmq$7k)OH_(b+jlx43NzIFppU~R*wf_*&s$uvb!K3-i&e7JBfC)|ru0=P zxyQ==$6kbY1B#(zExo<`@X0poR^EWdpt6jTq>hB-57w2ugza)YV&3A`SGSsPI#fGZ zwiMmYIu5^IO3{``e6bOQ7($rmgz3!F+kp12Kn7jEt9}e+~kUM){hMB z0?RL%fJ(<1VV-zrEr5Q2{b^gNdf2v>I)Jo~&7RF6n+_pTWRr~GoGAmpe_DwxXHv-5 z9%}Y;7~Y52=lW89K$EbB*7uUSq}#hOUNQ8|NA@fA&+xL7Ao25cq8rB z&U2rbzJE?CK^C%If8t|nmFQfs{{Y!&x(-*kWpYxh`Vzsmo*f`|$URZ!g6@EBd`!yrT%Lx=5_x}JUoyv~o zCDX1K1wxWWMhA{#3bKw_fx-O6D}ip-+seJSxVKW0!y-!YakZ6xY_2_rPkN~Z zrQ%#h?-PN&Nnp9-J*dz+7H*=JVx}}KLF56~ijD1V(Fmglhl0185e?U9Qcji7@LK*!Var^RPHZM~#`U}TY;o;}43 zO8p4*$z@GNMZBdXRA8()BaYvdZ^wP9#4y(~T#SDcl`HswKUx^?X38ySjms>JpYIZX zojEOI9bsNQP*gK(v?;d=0gOhOIXD^5;Zs8g&Iuto#{gtfxiY&(WM(t8K2|;7F$xLB zexKH^X`ER~-zj8ppcUi{AI7e3(INpY%+RyESU`viY<;;OOdS4|WWjgk=H@Ava1L41 zZ#d7_jGpz1W|~;fx|&#kcvWTukOGsqU=G8cl`I!Yw1$n6P;z&!(mw-M(NVRxE(eS# zlOj^iRH~d1208q5`czWg01GXl+a4LDE)LO<21nG6l+;1K<1XG(Ldfu)xE~?hpb~nY zUTV>{d2J+MH0iEY zt0C@PiWwv^xw?Td@+ipUuN{91mid{j_8CmE+uWwv8bVc@rs2*yXV$7QNtY(~5>^Tq)K*Y6+c zPB)s2g!xzOMl+ELZtq;Mj_}9NXk#Pr z`D-^ex{C4aMDywf=Go2Dv5ZIygsF?o!glOAuXRgQS#BQMB_NO{ zMh%Yt050?GRcF?2E+J@|=SgVh1xmcdbM(t`_T@4g_vSQk56_4zfd%+;)R>$F)0TeCPl#*eVPzT;q6s}Lu zhagktU4_c}GTPqgESU1{G3a2KzmOuDZZv5K+bzUH)IlI0s$!oq7bM=yk~Gq83B|+4 zLE!D`2Yy8}STD^L?&IoYQU3t4)_;g?A&W-1c&<&w z%M=M4I=(W)I2av?qfUWob$1ruOVRH1yC%3WET#zAl+ERoB|$wo>)MpqXyXy0TeANE z6DFWZQQYIb7eJy(7M-D@h6jP1ZoNOy);6}*)=wC*SmlL>nah_T?!g4^J-snPV^FrH zwbh1+=J{ece|NxW?}D8A5uUYG*GrLE-qT~F1=zPLRQCsl$5Yhir>N9!wJgaNo6PXK znX(Z70HrPhF$#X^z~G+Ux>b3g(b5@`Af9SO&lbzyF-cL7Fi-TSaOEiYeA&vkFQ`KS zXqGb?v-x=~yFBL|g=u*j8m_nCks@YYx6Z(} z2>=b=piy)RFQLuZ+e}pzqf;}5D#vha6&*IP%~+bk=H7oHwJQi@DjW%=Y~!D(?0b_{ zZ3%5%=xOW&aSWMJ&(Fqbo6k$r1$vg%dY9Z!KB;7BN7yyb+ zdEM7McBbQ?KIIYet{8%Sk506r;UJ9Q@Xn-eA6{ytuNxUBu7kwFc_oO*ybR=U2**m2 zYseqwk->a6OEF@^^*oB_j2boaA8saPSsUb8eF@}o>FjA{MtH#tLy@?UMldnYKT}ra zpj)vKkfH`c+4Av(9Jjgj??Mt>aHNBrw|=aCHFr+SC%&-VDF zR%VPZ8<_HW!Swzw{C1H$GWnBMeK|yTj<76^nbZQ#K;&>iz@#@fZDx?p(mC?^CItW< zn9d13sjywb-}jeMB(uvBw1G>Y!2<`Ll&;Jkb-RtHxP^x27ds|i6}TrOf!hO+Mtf3P zY7J^VuI(-FCbxpn`F}AD=Y`q{3@~=FEPt*!>rl%)*A~U*hB$+=FxW;$AB>z2dZh2L zxr!38SkNgL19owfl5y)=Fi4PE9s6aF<#2ZGUzKoq2Pfw2M@nw}2NGEBEegNdQb$k- z!np%IzfRw+Sea3yLlUmTf~v=EdS?K!$5G$C9SUm9ZPl*v1aMp!5(E)U7TCyD*FQ51 z;F5dhklN3b@~>E{WtFD2CRXRGwV>RO)yy_ z^3p68JKJkx*c=m5sfm)tCbwyBW|DETBI>?aUHy&=XQq1QtO(uItJ|Z?xT>K9o)kAY z#zqc!^y^g^Sq)h&%=_)e5DbiVwghF11swM6%?WFM@xskxXx5H)`I8vFbI^g4obY?8 z_Z3DdE8H#bKeOd0PmapbkQ~D#lFE$78D-i?2cX7y#a6t#w~^$PtxdG|0b`xwa?wA{ zoZyVBWkDT58Sg=TN+Ha`(%5qL;El?1HtsD@4S|juf0xO~=xK?jU(@CCrdy99X#hPz z3IV|b<~$F)JJ#I#j-`DYO9iSqUy!HSM5LSp^DxTuo`8exNvn-Cin?B=vnQ8v1aL^l zayM$04BN!Rskc@sA=kumKQ^-C@ z+1Y=(dSk9KDq3Sr6s?>YMXbvz$g;Ne11wGs2Wqrr+acW(sO(6={QJ@v{jSgQ#>~X@ zBl=LObiny`+n@MBU4WlVnibeyD1`mKJ;v)txeUk3hZK=8>j=ma0fL@Tfuu}Bq(TGC#lZw=}J45d!}e!?IF5-&ik8G&V z>zc`H(zjyXD`_5Qa;gS0PwCg1Ep|2Vnlsx=5C&+5NnN0hLHv4CCk7>PC<^;X3;1H4 zn3HUwXDL=N?a7w_4D=u#znQHWQ6nKzMQ831gZk!)XeGUhl3W{>}?bjzqm{x6&n_*7G$Ff3+fP{$|V2kS$ymC_bBlG6U# zNKA#7X+W_DC*>m>cP>Xc>r6JE+Bcio9@Zs@0@n;1IL=R?>(aGXv76E8OpwblZ{LHs zaN;mF{RK~ND1lxr=;WQGlo`%GoOP^TjH|M({hWx=`L@o{wg5h2gU_+&@)a4$fgzSz zWQs6EVaYo^2RZ4|w6olIx+<8Qt2|`kNiEn8dQwKQ&IDzS4^l>PRA&vzzR$8WDtE9W z7>o@25;6TM6Q@r+!wRXi1GZ8ZcV2VfrB0g~b-8f)=F-`cWsA!~xLJYQ(i+azj66yWalZM#W9j&O2E1mOA)Lz!-7 zx{}5)?D1Rq+m<722+27hNJ%HBf9X|WJqp(hz!eJ2&x`_kcd7LA5x1UEAwE$I@gnEt zI8bxR&U;X^jSRaBP1921-Xd!D1-nZ9((%#nF&+xGM=^)&;zd*3B|+dHx_>HZxVKotzNH5zBIHE5+l~`B8OA#|bDB+{SLix6 zb7~eV#(lAueydeVlMlFMG zbH-S6jAsMhm4_BO-g|3=B725O5PkTzjlkr3fH?$Hf_ahbw}NRS7|gB_+>iEnBe!GP zsU~T!Q5QzJl1T5D3UWu8Gmt>&I0KSFJpOevPO{hth0IX-aF&$I8?pIu^J4?K1CHLj zQfy0IO7^#k$qPy&4It;tO~ee4N_{Xnsn$5;jxRpwK!XcmCH4#FAypTJGS-&bTtH)c6n%?=V{zXm8Ae_Y?^$ZdJMYWiM>9$Rr~uMgaH7=dDdD zp+gh?M=;!t6o{afJxgS^InzfaofEs z66L0(*A~~Y-$fUfM@~9f(7A&iG0}_7masD-S3$CM3!qJr%cFF z+_+MY6R*m$1A%}(>MlU~k6NvJ8w(kpXUPdZ?Aw$U~=1Bs;2#J9p{KF^j4udAC>`W1Mc-m9CG`BkmD2N(7jC9D{3Bm7H zH;|=LDI;bZ9sba*y-x?KgV3K!mm=7LL~B^>ZW8X+MFVNum3F`!><829?@vi|>$w2B zoo&ly2kt~`#!B_U!5-c5Rm_E|6s5(zipLycDDqz3ei3;rRC41d=5M+^J7S%bT|x50UWf1@fC(z=J(BmeH?{}%& zkF`DirLo)$PYxksP5{X`BN-(@)SX|G7<hRwAHd{{RcHGpumJ(mlAieUdVrq$WD<{X0_i2WB|MJJ_|>VEL;m5};S0DDesPd9z!}9M^&xoXLb83H zSmP?Vlqbr0&NI$UI(G8@Nj_%JQ^EXyT1;dzFhs-(181DDZ2nz88gQ0qHce|WNn04> zAxA8f9k}$W zR~4+AHq5g)`Q|bX!KE!jNa_e?#zqJO8K^0%jPvC|<*ovI=9I`;2yZN@>Kq<_N@C?i zPvnuZTUeW)x+B|<%9I3C#BmSyL__>24oHi&TdGEVjA4JKY4SysRCQ?-bGPpS^rGR& zmVz65Mo>J6Qv>EDSfA9?O>Jv{7TN-%_<|qtsnlN1BAaVd{<_j4I`D_}=A;V%1IxCz zTo07_3V)c$r>J{4lE&=6FP8~i1BXn3_;fWSMrAvr2pc>CRaAbI70QRL?y0@Zvg|#u zLxMlY+*AbvFbrhf&~5~u=Rn2;Rb#ZDPsWv$0z)t$e{r#%vH?F~ zxc>kOor97Dj!O;zCy|PaYEUqC`9UN%@Q#!y7!H6E52k-g8UUX%s|EeREVz?j<7m_U~pRs(Mp?to<$%;k~Mhcgu1qVX%u^(m>i$Ro}qM48qeio6|i{619z6Bf53^! zsb!A-OIO`By8t#4RX9Gm2PcdU^jI#t32L&5q>?*`phpKKom3uq0g29ifU9=!!m>af z>J6%QNr_@y5Kb~l&jSag77cq6C6%mFN%mMl5x5>?H=`1%AKhla^dxuaDa~yaplK}b zrXoZlc;%Wl3PBtYN6nC1o_lqo&5K>EM)F!27E^e{GO90;wRWaY3^EiPp17rr?bb-e z%$Cb;gfcVg924n+PX`B#QK;_GOkTrh1#V(?l+o%W}5%WQ77~H)EM;@IGOy4G7?RRrcwB&u7B@9nqgCKr1 zxq{t_vFgg17giorfO9-@l{o(ZXpqA{jW%oOC35L`b^*uC)^N&4Cyb7J`c++ZLbql| zaXq`q2z#sRfUWz-R1lGo)kZqxj@URgZXHD+E~$U4q333GYi#ulTY%|fyh=- zAP1pP91Qy4Qx2CLu?BYz&GQihBaf-vQy))yYD)T#UWExQ9wHVCE16>)ZdBxR{>foh z5ZOmKLwDsf0l6d$sJZOfz|ZH>hQx^(7T!f$X<(g7GVY38;~;~|`x99~349KIZ1F{g z*Sc$DzC)`h2abCFv?`*p$Li;w)YJ@oQi#cH754R}A$E{vGE|Y1?LZGz%XIlkIZ;w) zBo|T}1M{RB43W=KjQ7c;4Uz$ndvl!71CHXmbJwm;DF`Qyan~eJ1LGtXQ2S4E20;|y zcOIbQ+Lgv3Vu(R0gT@It{XeZIKry!@bRNHrP_8;BbT|YjA9RoLsSU%r7^Gz}?%Xz@ zI5<5sNMl-F`;JFI6n|4umvXyU;2xtRngqm1i=USVkA5moB?bc+-1X~^@Sq7IZGeST z$;VJQ!Oc6&C4s;L7$<2t{b;)$go5Fp%Mv%36a@%)tW<_K`SnUB3 zRG-KX=4cqwv8W6qD~tn)2|4;?Qnck%1&x(>$Ibr$*Q!iTD6V#}+*b!Y@z?dB#cbc} zq6C0(g&*Nn%uguDt)VU($%ruiGZ@JHYDcz&9q$#ZOSEHW%s&%T%r>7vrqa=+b~ezn z6aCJ=r=>sawqs%YJ-0n`5I@q6W-C7Al0$AzTHe{$7{gny_;L7Rks^T(R@MQ>M2UI* zKgNz>uTogTZo!FFcmryxj=sxMtnREIVm3P}fI$BMKhCALG3|LTmO#p&Z@S97{SH0q z7Yiek<`B5!e_y8*uE2@oPc4xS;N5e_PMtZSV2w~>H5lj!$uwMIBoQJVv4PJy&-m1c zhhY&&VkB^OgOARg#bH9qR0fyLZ|{@~$Iwv7RO|!;o@p_pNM@Oa*O}Qc0aD9{SV_!G`b0)u40fR2xNGVQ7Baz z?l}6^CZ8XhcN#Elpb~N00Gg!r2c%6g%7wt>`qLX2A1-ox5l}v&?Ob3f9;BR)%AfLt zIbo61cKoRYgarfRs0&Fh*sa6oK;dgG-r36Dn}=M?o| zr)xRJGn4KA0PCaztCq>mGu-r~5`{Z@k;f`{{Cm>1!s3?QxRhc~A9j^l%i%M-k-!-g zSYjp@cqE1+2XW?+oVd!U10O?72$nU-`COi(=HjK;RWhK4^a>mL(;5_TgkT27PEQ|| zDDwgYSHkB!4zz)iqA!*Yn48NVl;?rlwE=Po80*`N&?}0Yami3e9mNY6F@T`}JYzk3 z=O%y^qHa9Ia8C=kWAfvX$flx$yfZQlrv*@)pMTb<1&!px`EQkQcMNgTnj32Trz&%i zyA*~-GxK0%lhF6405G7BnT`fe=|!vq*<)bC%LJTcG5K-y{HcK;;50m|0-`yDVR`q? ze;R8*u>GE1+$ah zWQdE9Fh=pWxA#VA12E&2UcB@Cg%&`Vs&d;f>FNzRXN@94WszlLhF>q`P%(~A)c$_d z0K4C2-<|f8kKJGpMtXvOl_^qNIOA}^$mmTg1(?>{KJMHS3HiEGNw@6y^Hol8REEg? zG5XW65@p>~?j$w_O9P+tfl={*r1DNjKZPOaI4g0_Bc=znCd?`6jPMAi2t`-iNJc#8 z01^-5N|8(Fd0`l_zzdFYdQz~-TzPEB5stFYmnQO&|}rqAIO?C4ub9l zS=jCw;~b1rt`Q)RMoPN4+^nuS9Y@oSXs{9SpO=6;6PiYl<0NDba4|`Ou+)?|ETw=Q zcn7`>2WcRoJqAg@$Ll~?Fg(MCx~04V^okPjR$y)tenRbE3MZZS=D=o!a7w+^1P zTm}-+^1ytG%m^Uk<vTh>)DE2(m zTw%AIHhTM-doTkAJ_zR~fErXYU?AI$ax;o@s)pL1HhIf-{HVBwAC#UzVeDupDg!a; z)ms_*&@t*hTmnhwCm92rVw8=!0aTxQ00R8Xz#I&4N&b|pka+`d=11p1SZR5QA}E6( zCzALeei@|>eqvV{>T}Y7(2Rmq5EPTwj%mnxZ(hEK^rFB&Dx6_QY+#c^N~8O#d-LD% zrU226i@BSX%U}f~(~vo%@9iPjV0vT!0Ixvx9~lIn-Y!7yF+f%1iBXSX>H1R8okw80 zx+7t;!vUOANL2%#SI{149mlJch(*8TQ=Pf%#yVr#npxF@ZUBzp3S%1bN?fy;!=9K$ zT>e6kGXOUnXRxLrRIwxO@tvFbBRSeAOaN|P%nz;yH0I9Saz;l7r3_=2DC7u~mLALo zKNC$Ma6wf70(l1~KSSDq4pa>sgoIYe!h}CCInO=00+VhR2chdsb{GK%Bjq6RPZ2lp zh4)t3wH<8>qss=733i!kz-NKqaX2HH2V=WcUN2o&VR z83sW($3L&N0?-uzY=Ck7&}jLKV;w$h0x69y!&Ua8v0bC4O9CmTF3M7xKa*0yk|Hg=3nqNYeFWvBHtsEf$8<9D&AVc7&#augWX8&Pce|H?gl#@qaT>-NRkk& zC2Y7OA1Kaq_)`3;-du_~=(|FWo~D^VA_}Yv5Wm8KzxwpxqsiN}FFjkDE=bUMAcEgY zQwrn)NXJu-2jxJ@?<7{prsjBvJaTcn>DHxWXT}%h^c;UWb`{3~^L(rZ3G2=})QXcl zrIHaNw*gAv{uEeae3KJ8$sd;aKjHf^2F4g%BKM^XvJVmKhYvhY01!-?|{g`(%zd^rdpIp$Sl^7jhC8A$IYfN@*+~ z3Qrk1=|}}K1|0&oamVzf47em@fImLGLTM)st@YoOT5 zga*!b?m5LGyY0q%bImG0m>M_Wf=jp)4d?G2`fvVG@iKN_02wRfaP*X)=3Fg zWCuKU%`o-GH!$>GwLO~JMTPk+3B ztti}uj^{_bd-VL1Nr8a94ipS?7_z4-?!^eU=>?#z!a|0zylrWTjkC{ z03JylY1~Lni_gk8p1=nqWrzywC2q8`axy?<$X+}BQfKMYa=lWAye*P2*t2(my} zt4|zcbPNg4zXLz5IrlIbkYlh?IHc|Zoymlma9wbj0A%D~=jobm&*$AM%?%M59djM{e$LX<|$9R#|NKU z3lq-M>GR}JfNHWy!Vt$ky+t9$86PeQ&fhR{ArHja4^h3$4pY4tm?p^?e}m$AO8SV03RfToC1C0 zQODs*tfYo$*8u&{0-RF|j!?{@Rx)G+l24(=KQHT0NV#k&JHhA)BP5SX2Azi|-74qi z9b2jVsqDb1JNGUC%>pAu8Ok8~ai7YcwcN05dz}9Og&+|@3<|I%jsnM>-(TlWH&+jJ z7oX-g$@3Qg{MkHblhYIpHPlydBHrLMXB?>*;A0v5X%t6-9Sf-#3<%?nI_8$b7Bmt} zs3eVfIB7Ao5!12upoLkAB&jhR6A{SGRfAL_#J*u8Ib)mvJ*q|s3ZOfV@0=AN57Vtq z!@c(i)>I%UaEdXNQ_r;{tEei`xELgpo(TPDVj8i$rBX6{#^3^^^Y~N5Z4ogLNf`&_ zZZX>jr!)Y?E0U|uIovn{^Ax#W4o9gu>p(H#h8U(t9Y=ahZX67H6VvhZrD2kWl?p4Q zEWLLlD|-8J+MDxk2!!(740SZc=tdD+VO@l7$;oW~ADt_N$YqE|**q4`Jw50exUP*F z7F&dF22(B1<|%>FHo{@C!N)nEEIE$C;R~_A#zqfOp7e#ny1My^An17G^{ICnNND*+ z#av-}QM=^%c(fKmdL;qG(fU zywQ(H^MZdN%~qnY$MWBZ;{%=ojOLYOoUfJgs2z%PO5;M1IbKg;&N-k;Vts@HjNA8P z1RldY@&5qUr*sa(T2mnV!zTyO4k;bJP!xwGcA~-_uE9XZxyMmTBmnJ6Rf+oXNGvqS zP?6=djCjJ4)A6STw~&@L@gwjz1^JKT(wN!+u^|O;+m9jJH1?H@sxTX9!sDD!g?o)h zRKau&^Ad6nKAakSu1*3X@5dly;C^%p$q@jeUBPpK%bsaQe(>B*I6joD0+s~s##A0N zPFGZqYNF@4!RtU-2L}vt0q7J`8Cxrs0Y}O=4yKqOjzSJHS145cp|O$anj{<15=@<+ zFafsY^*uixC;(E-r-BpT(vi?-7%W)hj?^R}>n0c}VS|jRTyafB+AyX(f=?_)Ir`Hm zla-kgGTmDY1_u=c$tRW?2@GVPn>cL$0GtXe8x|61{ICtJvz+68#@+bgsiV(doTG*3 z1$O{H8Uc9?GBIHCf=5lvcW3j>0YN)cIVADBiade&4Uns<0}}^U$&fe8kEd#6hb5j^ z(KyJE_x9^RBuyNyAarDnqh`ig268{z{3t@Lj1>fS1f2dxkXZuk--0%b8lJCJt_ot?PP>6&4v#7PRQi8)qYjPZldJY&B@NTCZflDtM%>Zumc+3&|nJpran z4ABK;h)1)GfZUPm^M14ya+!5T+A+U5QONY~)2O1tzd|V)p+f9PUb$TQdUv806BTJQ zy^m5k=jv)vR~|x*(J*h6^#=z&*y&NcP=_<@Eg;Fa%9oUkdR(b)S@un?05P>Er4`sd|@^a0=MJ_> zaz0-F08>W$0I|qqQ}UACzFsO*m5>6P20w@lpT?HKiERLlU7JZY9qcwW5L`&v$z#(83-sDQ3XsTrrw7ap82}Pn82mU?eWY`@X&}<>!j(G)@4MEE3e1j1 z*igKYLWlU-z`+B#>}tR+$IT?m9)9+BWPNBJ!%pBcAVyr{pxycPrsYXKULyyCxrgV| znl1oYcj1g|!;Fmc>C|SCC5}-0B6iw9+%u2Hm{@bTF*2hg5Xa^@+()*1QwcjmGcoEp z=dk>H)Z7S6fnz>Uc$*`VN%i!gM&Bu7>$j@|jos;k?lw)z-@8N^cd*I`9KYUl$@D154?kbZs9FlK?&L65!6OEj z9%;c>Y#pGg;EuGD4M&KG2LyyT1&aQZ#r@k9`EmiyD)vO|D*&>RxK$(XV+0Svq9JyJ zu&z!x>r%>F4-y0jL$2t~;gCMH5Z*pyQIaq~BcG_Lxk-->N}h4I@0@#8h>}^LSQ0UD z8x>YyH*EmtsHTIK=!w)Y3R?(Q1cc;!0(t#vyG6Md%9)5zM%8w}{d1lvsWdedi4kCq zLSu?Xbp-zatQjPIdF|iUg8St29cJ7|%(AxZdjKg8-eR*Woc!p!una)~=s4v~NL8Gw zDk))tPn5fkJxCNLD_Cn7##Et37{Od}KAor;Mw4Lk1B?JiL)d~TFc?qE_gz8f0lE)w z&YZa|`=>i`lHDp;3bBCSy<#)KC5a#Biga=rD8nQvoWQk(CB{W?TjQC{K#Hp`$)A&-Q)l=5u7%9jM9jJj5P7d8vp~LY#ja+4e0b8wYGWPW9hi` zAB_x`JcYNM$JYwy2aIqrMxuIZM9fYHAcO12{{UK|TX<0zMIa6`ag34uDT?o*35}(U zc~X`=7_i&V;3-le3Hh0sI)DaoO2uDJR!*$B8{2Oqdoc$;ORy&!W zrrt0PDoBd4&&m(?f`Nnhwkg~!yAsUr5=H?F6EDbPlRLdh0Mv0y5)~W8C*EJ!|?2Vg1{`Nrpsn{6P8I=@+fLk0K5>FqEO6tn%9vEYwBOhG$>rDou zWi}H;h7bdhiDNs8@Nvd+N9Rn7Y!WWaC_G>;;rjj*Uit>xjw)T2+_LUf2O%&y$A9zE zogoi9h*TACn>`5bb5w}gSZNOYA$IMB>xynkBRSyZS&ta}Y9v%|Vi5x}6TcV-?;K~= zq(~cP@;0#hq<{t}Y-zQyf;aC z&NmYy~Wc+8j{R&a1IC??4X6rN*{oZx)Q z4l+8^93L@EZHUL-1u)tA(2#l(fZH2uunI>N46un0m>t!J9Zx^4BsAIxV@Q>hAp60x z-OhT_i4={?xRvR=hvy`n@mZr!nd5x36|Yhk&__E$K+}@Vvo0K@Jac7DKrQpc$+1oW>P=ZoR7w) z5lOL%6lLJ&cqgYy1526GAY{aVI470wQh71FAQdXWVMiFM)dl+u7VN+3rA5zBRGd_d zA0AsHlwOQST1Z#!IG-*&9gCi%^a7Qgil|e&=}n-}V2w%r;UN2TAC772>;}a7&~gb- zdQ^v~fxP4%V+ATX!-dDTKl;>&Vk9sx?SY;?wAj_wgC>kljISQ>Il+E^N_uPwwaVwP z$>N2m3hb(*%wf}v5mDht2g>i)cR0`1m=7-^{DF$bIRiiAN^k>pRqLN$&XBLzVzAk; zsPzVsqfYDqY#fz1S_Vp?26uk+_Bq-Itv)|DmW<$Jj29cfFG_j_qZ?pWUoo?uK^z~- zoFq&R6asOa=eJ&H^b$i_U6(Cs^2kD~Iam2XB;`&$02J^+l#R-)6l_DzbJ${=RvTT1 z$sCG5daW4)Cvc{;hmwiCumBmrCX)eK8P_t6ynWDlAImh|>k?)+IUjd+bDzeLfusuR z1_NM_2Ts1f=dCK7@(Am}7(U*#!_;tekZ*L}LE|SG{PHR`d^iO+WA1P`$Kg)nvJ|tJ z%q;J*jz|rFdGyEhp_SNfXF{KKHoiz0?mCJX36&TO0Br8SBR>A-kN{J7$G_XZ0Qm%= zfPXPUj>K|Fy3>(dmKglDijF#kq`5Hj0wOyl3Z6V9P)(1RcbI)o$Lw$lde2Jrx z@Z}#P01gL1kxM1Sj)*+?*X89ALV@@I26_Jg>(aPcb|MknG(cv5CVF zPSb%z#785h;Ug$NGB3)&f$Ql^O8rKKjy7Q-WMymui@8ocNI&CGjGh@CKqK!1{xpM8 z3zAB#ihEHFmaCz1{iNA+3j6tR8z|u z4MmVhl};#u1VxGx;6g&Lp-T}2rAP=xT1cp(h;$JHAsDI@K~O*-peP_nk&*xs1eA*s z?n4qff|nxF^yRL%-g@unTkHM3^Ka%mKW2Sv&N=hVp1t>6KD+IS=)-mzHgcy%LDNeq zn!uD>kM9ZW5|C>+^)4h&O-JNmKo;6;aVs`l0(t>x>^a}3)8V;-#;w3N+3VZRp|mMjD#iP0tWmXheI8S# z%~7I`{JfiNmnq2ejR?^^#+C3ME6KD#>Dtyxu4bSU(-$|rEy*VPld_hv3a<3gRanZ7 z2<$3TgP<8|9*A)mhS^z$_za^XxNdMHC$!WL+Wu@#@)cKQlV-`~#PMqmlaW5~0q_ec zZOe{fwnPa~_PORFQDeWc1SGsV8vA^e^)vAm2#q{~Gsum>I!d${q&su2Y}d{AstTbs zCY?b-s^Y6bIcyRv=M8*Y?}^eFW?eRmne?VKdF0XfLUJ5)yWA$SNS3{h0$|H$qE{}? zA;i=>7JSAA-I|ZiHTnR}cw2q=HOp(XUZ*{jNog*nuH`)I!+p-ZKq8&?)wr8N96FHl zNiT#Tm3>C2{PU^XJx{E|L=ss#_IR|u+>*lw;Sk>tkMJ`>AhwRPi)y)+GTj1GIdV$u zCbN@g->7jEUTZObj;vKp2!fn+Upm`N9k7Rix^FJq%%Ks0@uts00WoLwZm`g{NIS;~ z{Ib;J$xz$TjSA>&5vl5HrSfdA zV%6dwPQn;alBFuQ<84IE-;3Qtvsn>?6&@BFGdG&Lkn-fO(Ye-du)V^eObo2?9I z#+uH=HyH+i?!LR-_UQD+m2+DQLfnNVO_CJE*a~5=c(lA=!I5j;bnB1K#T=;iIimWd zkn#i=<*G>E%g(7d?X+HR+*-+^>{ z5_E3EAEN>&ikqL>K_TykX(pmOOfBz+MCuRD3WUvIh{kI$ujenK6gi;__Ug(Oic*v^Nb-H8iBGp zz!jUI9-}xzL6#iEeHIUJ9vI&Pm?In6_~cDbf22R)2`!tsyZa>HaosP_oWi-C85+zq z{|1xy#H9@yT}}7}n5_+xg|_^qiy`|(TW1D^NbF<)*JCeWl!Lx zg3#$9&a4l$gO;z9-Mx5=l&%;b*7REcj1msXcwV)TNhUp8zZmpf1S4kO&lK!~8(qBq z8f4FZE%)~m&h~X0J%YF4k_>V+H^yaMwLhN);s_I#uiKiqo6Z}|+hVVHa3YH+3o{Ky zLo>2p2`s~%Y#2SN_Ueg-7g9ap)BXb4KOo;KBq^{#VHuYG?{4h@dk{u<@kUjI4JW!s z$Xq0fgB*K@yuZHo3!rAW`Fn$E#;|-rsGsN3h*M7wloKonYxhJK=s(Dm+K|1DoaspCEwF)xbm$yjgRUgW?hNMDL)5dP@EK$t9( zyAWpuQ|nx-&k28;#s)s>5mk_x!_cde(j*TqHNjRE)>WY9?M}@p4A$aYv!l&Yn+HwC zMki%UaK^7!wM~+7CD|H%`ty%Tg$!=cOae%frWdm^mDi}3eS4ru@ciN|%YC%fn^cK~ zouc}BmZUBnj5iJFVL9|jE1duy*8Tm#l1%+tK$c&cQ!!Gd?W+XM_(-W6r9)4L@!E!~ zb>e28BW{n&(_q=%_nwSk!bXq+!@mFs|7)ECCSzLiPw5d}Fm(4Ggc~S=mXIBMqFs#3 zu|kiSKP+~&6CU&A*k}pW=}K{Sms`kyKD^Uqg4-`CE{=7{lW9=!#|qgj*r>DQHJ5Cu zWv!vv#x)(e8WTq6he6}P%f(0z$H7icp#BVhlK z5oj>a5=pI-4}bt|4x*1wNHx7m&8bn0sWUW|lb1y7Es3h_R&^WN!wskYM*MnGsRv?XJcy=*W-pAb{tk^Kd3&3yi#$k$` z*U@A2L*)qET2ACth3c3PPSzL#J^TeYsB1hcQY1W&eI({$mfwdq{CMu|@%x#`tZ8cX zq8Q|Is^KTz`ygper4iq1jdb6-{=pQWsek4nak7EKj3fQBQhe&tV&G8taAC&4$^B_` z4Pwlw8q|E&mjiMSm96N?n-J*hA=@V-Fx6P6lYg>p5O5jb!#vzMRtoMNeUdn$4mFn~ zHO7id80f{W*xe)F&r2%Od0GI0qH&jwz{I58?-eZY>p^IMLA}4kvMqyyRWN>Z!>68F z#ACHfk_gp0?W#VCG9?;ERp*W@S?KLnH9cm@fTn>P09Y(OCA|%a&dEQl)98-@-`UM_ z52|jDgkXEIbx2Mh{vI~hT6+sME}9g)Z`133epnqP2hI?MT5O<+-^qpI5m|SChjY+z zmwdzNw=@+Jwu2*Ic6lb^%GwzzlmPB23{gkpFkeS`uol|@yB$mc6flm*6hChhb z%2G~)4M`B$=2BYtZ>;T#dZ*a7A)7YM&VOfAMTDsgT3)-E>=@|G16)m5QF||UZMazG z)6FlbcNG&qv(NePb6-uKa8NB7));CxU25K~QVapc)Y8mkA49c+8pnC>@whrAB|H4J z>tUmd@0E9C+pms{VSZ4L41yp*C@)&SpfPlt`13ZOnCNs{oJ33gA`~okxi6AMwLiRc zADO=?9U{+*oOs9M9XBS)640av(+448QCjPkz z89^qN_wy!1Ni?bv3Rj>e)}YKh83cy`OJIaf`dXc;YV6`q*vk#&?dQLU8w>XHcDYpr zZM0$}zd4W&+^wE;o(K{)!S>=`?J0Wk>t>f{nJC9?wU*d612#0i4jC>MOLay=L(str z=`muffG}Dc{n3s<_RVbN*Iqea#C9uS3yik}hs^|dB{g={%4_fRb1}+04@kMg!o^#y z7esQc9#uAao3;7eBzs00xALv&l|hu36mtXxUI|E4UoM7A39G$U*3n}Op?9TOugkJ% zIybYzeKmv>l`>!2zTB)7EqmW9`}z+&+JPrS8hledI<77;~G?F*C`w!bONZ~ zX#!hyPSy`b&lsw>hxPf2`Wkb;+wUqBn5qhW#dJ1C;gx#!R(?#jk;E7*KvJZGT=^lA zM8x*m955yZxPkzccWEWPlH-ruLhsiC z_Y1-V>Vo0EK~evCz|J2N7 + + 4.0.0 + com.ossez.usreio + rets-io-common + jar + + + com.ossez.usreio + rets-io + 0.0.1-SNAPSHOT + + + Rets-Io-Common + Rets-Io-Common for RETS API + https://github.com/USRealEstate/rets-io.git + + + + YuCheng Hu + honeymoose + huyuchengus@gmail.com + -5 + Open Source + + Sr. Java Developer + + + + + + + ch.qos.logback + logback-classic + 1.2.5 + + + xalan + xalan + 2.7.2 + + + + + package + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + + + + + + 11 + UTF-8 + UTF-8 + + + 1.7.30 + 1.2.17 + 2.2 + + + 2.11.0 + 2.6 + 3.11 + 1.4 + 3.0.0 + + + 30.1.1-jre + + + 4.3.1.Final + + + 7.0.42 + 2.6.8 + 2.3.2 + + + 2.12.4 + + + 2.2 + 1.3 + + + 1.9.0 + 2.4.4 + 1.18.20 + 1.3 + 1.33 + 3.0.0 + 2.21.0 + 3.8.1 + 2.9.1 + + + + \ No newline at end of file diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/AttributeExtracter.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/AttributeExtracter.java new file mode 100644 index 0000000..4eacba4 --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/AttributeExtracter.java @@ -0,0 +1,62 @@ +/* + * AttributeExtracter.java + * + * Created on October 28, 2002, 3:50 PM + */ +package com.ossez.usreio.common.util; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.util.HashMap; + + +/** + * + * @author tweber + */ +public class AttributeExtracter extends DefaultHandler { + private String delim = " "; + private StringBuffer textBuffer; + private String currentElement = null; + HashMap retHash = new HashMap(); + + /** Creates a new instance of AttributeExtracter */ + public AttributeExtracter() { + } + + public void startElement(String namespaceURI, String sName, // simple name + String qName, // qualified name + Attributes attrs) throws SAXException { + textBuffer = null; + + String eName = qName; // element name + currentElement = qName; + + if (("DATA".equalsIgnoreCase(currentElement)) || + ("COLUMNS".equalsIgnoreCase(currentElement))) { + return; + } + + HashMap attHash = new HashMap(); + + if (attrs != null) { + for (int i = 0; i < attrs.getLength(); i++) { + String aName = attrs.getLocalName(i); // Attr name + + if ("".equals(aName)) { + aName = attrs.getQName(i); + } + + attHash.put(aName.toUpperCase(), attrs.getValue(aName)); + } + } + + retHash.put(currentElement.toUpperCase(), attHash); + } + + public HashMap getHash() { + return retHash; + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/Base64Encoder.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/Base64Encoder.java new file mode 100644 index 0000000..d37dfa3 --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/Base64Encoder.java @@ -0,0 +1,230 @@ +// Base64Encoder.java +// $Id: Base64Encoder.java,v 1.2 2003/12/04 15:27:03 rsegelman Exp $ +// (c) COPYRIGHT MIT and INRIA, 1996. +// Please first read the full copyright statement in file COPYRIGHT.html +package com.ossez.usreio.common.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + + +/** + * BASE64 encoder implementation. + * This object takes as parameter an input stream and an output stream. It + * encodes the input stream, using the BASE64 encoding rules, as defined + * in MIME specification + * and emit the resulting data to the output stream. + */ +public class Base64Encoder { + private static final int BUFFER_SIZE = 1024; + private static byte[] encoding = { + (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', // 0-7 + (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', + (byte) 'O', (byte) 'P', // 8-15 + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', + (byte) 'W', (byte) 'X', // 16-23 + (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', + (byte) 'e', (byte) 'f', // 24-31 + (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', + (byte) 'm', (byte) 'n', // 32-39 + (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', + (byte) 'u', (byte) 'v', // 40-47 + (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', + (byte) '2', (byte) '3', // 48-55 + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', + (byte) '+', (byte) '/', // 56-63 + (byte) '=' // 64 + }; + InputStream in = null; + OutputStream out = null; + boolean stringp = false; + + /** + * Create a new Base64 encoder, to encode the given string. + * @param input The String to be encoded. + */ + public Base64Encoder(String input) { + byte[] bytes = new byte[input.length()]; + input.getBytes(0, bytes.length, bytes, 0); + this.stringp = true; + this.in = new ByteArrayInputStream(bytes); + this.out = new ByteArrayOutputStream(); + } + + /** + * Create a new Base64 encoder, encoding input to output. + * @param in The input stream to be encoded. + * @param out The output stream, to write encoded data to. + */ + public Base64Encoder(InputStream in, OutputStream out) { + this.in = in; + this.out = out; + this.stringp = false; + } + + private final int get1(byte[] buf, int off) { + return (buf[off] & 0xfc) >> 2; + } + + private final int get2(byte[] buf, int off) { + return ((buf[off] & 0x3) << 4) | ((buf[off + 1] & 0xf0) >>> 4); + } + + private final int get3(byte[] buf, int off) { + return ((buf[off + 1] & 0x0f) << 2) | ((buf[off + 2] & 0xc0) >>> 6); + } + + private static final int get4(byte[] buf, int off) { + return buf[off + 2] & 0x3f; + } + + /** + * Process the data: encode the input stream to the output stream. + * This method runs through the input stream, encoding it to the output + * stream. + * @exception IOException If we weren't able to access the input stream or + * the output stream. + */ + public void process() throws IOException { + byte[] buffer = new byte[BUFFER_SIZE]; + int got = -1; + int off = 0; + int count = 0; + + while ((got = in.read(buffer, off, BUFFER_SIZE - off)) > 0) { + if (got >= 3) { + got += off; + off = 0; + + while ((off + 3) <= got) { + int c1 = get1(buffer, off); + int c2 = get2(buffer, off); + int c3 = get3(buffer, off); + int c4 = get4(buffer, off); + + switch (count) { + case 73: + out.write(encoding[c1]); + out.write(encoding[c2]); + out.write(encoding[c3]); + out.write('\n'); + out.write(encoding[c4]); + count = 1; + + break; + + case 74: + out.write(encoding[c1]); + out.write(encoding[c2]); + out.write('\n'); + out.write(encoding[c3]); + out.write(encoding[c4]); + count = 2; + + break; + + case 75: + out.write(encoding[c1]); + out.write('\n'); + out.write(encoding[c2]); + out.write(encoding[c3]); + out.write(encoding[c4]); + count = 3; + + break; + + case 76: + out.write('\n'); + out.write(encoding[c1]); + out.write(encoding[c2]); + out.write(encoding[c3]); + out.write(encoding[c4]); + count = 4; + + break; + + default: + out.write(encoding[c1]); + out.write(encoding[c2]); + out.write(encoding[c3]); + out.write(encoding[c4]); + count += 4; + + break; + } + + off += 3; + } + + // Copy remaining bytes to beginning of buffer: + for (int i = 0; i < 3; i++) + buffer[i] = (i < (got - off)) ? buffer[off + i] : ((byte) 0); + + off = got - off; + } else { + // Total read amount is less then 3 bytes: + off += got; + } + } + + // Manage the last bytes, from 0 to off: + switch (off) { + case 1: + out.write(encoding[get1(buffer, 0)]); + out.write(encoding[get2(buffer, 0)]); + out.write('='); + out.write('='); + + break; + + case 2: + out.write(encoding[get1(buffer, 0)]); + out.write(encoding[get2(buffer, 0)]); + out.write(encoding[get3(buffer, 0)]); + out.write('='); + } + + return; + } + + /** + * Encode the content of this encoder, as a string. + * This methods encode the String content, that was provided at creation + * time, following the BASE64 rules, as specified in the rfc1521. + * @return A String, reprenting the encoded content of the input String. + */ + public String processString() { + if (!stringp) { + throw new RuntimeException(this.getClass().getName() + + "[processString]" + "invalid call (not a String)"); + } + + try { + process(); + } catch (IOException e) { + } + + return ((ByteArrayOutputStream) out).toString(); + } + + /** + * Testing the encoder. + * Run with one argument, prints the encoded version of it. + */ + public static void main(String[] args) { + if (args.length != 1) { + System.out.println("Base64Encoder "); + System.exit(0); + } + + Base64Encoder b = new Base64Encoder(args[0]); + System.out.println("[" + b.processString() + "]"); + + // joe:eoj -> am9lOmVvag== + // 12345678:87654321 -> MTIzNDU2Nzg6ODc2NTQzMjE= + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/Base64FormatException.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/Base64FormatException.java new file mode 100644 index 0000000..3d04580 --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/Base64FormatException.java @@ -0,0 +1,19 @@ +// Base64FormatException.java +// $Id: Base64FormatException.java,v 1.2 2003/12/04 15:27:03 rsegelman Exp $ +// (c) COPYRIGHT MIT and INRIA, 1996. +// Please first read the full copyright statement in file COPYRIGHT.html +package com.ossez.usreio.common.util; + + +/** + * Exception for invalid BASE64 streams. + */ +public class Base64FormatException extends Exception { + /** + * Create that kind of exception + * @param msg The associated error message + */ + public Base64FormatException(String msg) { + super(msg); + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/CaseInsensitiveComparator.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/CaseInsensitiveComparator.java new file mode 100644 index 0000000..cf4a72d --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/CaseInsensitiveComparator.java @@ -0,0 +1,12 @@ +package com.ossez.usreio.common.util; + +import java.io.Serializable; +import java.util.Comparator; + +public class CaseInsensitiveComparator implements Comparator, Serializable { + public int compare(Object o1, Object o2) { + String s1 = (String) o1; + String s2 = (String) o2; + return s1.compareToIgnoreCase(s2); + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/CaseInsensitiveTreeMap.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/CaseInsensitiveTreeMap.java new file mode 100644 index 0000000..2d1a10f --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/CaseInsensitiveTreeMap.java @@ -0,0 +1,17 @@ +package com.ossez.usreio.common.util; + +import java.util.Map; +import java.util.TreeMap; + + +public class CaseInsensitiveTreeMap extends TreeMap { + public CaseInsensitiveTreeMap(Map map) { + this(); + this.putAll(map); + } + + public CaseInsensitiveTreeMap() { + super(new CaseInsensitiveComparator()); + } + +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/CompactFormatData.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/CompactFormatData.java new file mode 100644 index 0000000..8256bbf --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/CompactFormatData.java @@ -0,0 +1,302 @@ +/* $Header: /usr/local/cvsroot/rets/commons/src/main/java/org/realtor/rets/util/CompactFormatData.java,v 1.2 2005/05/26 17:43:55 ekovach Exp $ + */ +package com.ossez.usreio.common.util; + +import java.io.*; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.StringTokenizer; + +/** + * A class to represent set of compact-format RETS data. + */ +public class CompactFormatData { + /** + * Final value to denote that the tag hasn't been read yet. + */ + private static final char NO_DELIMITER_CHAR = 0; + + /** + * Storage for the column names. Set to null to show that the colum names + * have not been read. + */ + private ArrayList columnArrayList = null; + + /** + * Storage for the data. + */ + private ArrayList dataArrayList = new ArrayList(); + + /** + * The delimiter character. + */ + private char delimiter = NO_DELIMITER_CHAR; + + /** + * Return a CompactFormatData object with information from the specified Reader. + * + * @param reader A Reader from which to read the compact-format information. + * @return + * @throws IOException + */ + public static CompactFormatData parse(Reader reader) throws IOException { + return new CompactFormatData(reader); + } + + /** + * Return a CompactFormatData object with information from the specified + * String body. + * + * @param body A String from which to read the compact-format information. + */ + public static CompactFormatData parse(String body) throws IOException { + return new CompactFormatData(new StringReader(body)); + } + + /** + * Construct a CompactFormatData object with information from a Reader. + * + * @param reader A Reader from which to read the compact-format information. + */ + private CompactFormatData(Reader reader) throws IOException { + CharArrayWriter caw = new CharArrayWriter(); + int character = -1; + do { + character = reader.read(); + if (character >= 0) { + caw.write(character); + } + String testString = caw.toString().trim(); + // check for end-of-file or end-of-CF-line tags + if ((character < 0) || + testString.endsWith("") || + testString.endsWith("") || + testString.endsWith("") || + testString.endsWith("") || + testString.endsWith("") || + (testString.startsWith("")) || + (testString.startsWith("")) || + (testString.startsWith("")) || + (testString.startsWith("")) + ) { + processLine(testString); + caw = new CharArrayWriter(); + } + + } + while (character >= 0); + + if (delimiter == NO_DELIMITER_CHAR) { + throw new IOException(" not found"); + } + } + + /** + * Process a line of CompactFormatData text. + * + * @param line A line of CompactFormatData text. + */ + private void processLine(String line) throws IOException { + if (line.startsWith("")) { + if (delimiter == NO_DELIMITER_CHAR) { + throw new IOException(" not specified before "); + } + columnArrayList = readDataLine(line, delimiter, ""); + } else if (line.startsWith("")) { + if (delimiter == NO_DELIMITER_CHAR) { + throw new IOException(" not specified before "); + } + if (columnArrayList == null) { + throw new IOException(" not specified before "); + } + ArrayList tempArrayList = readDataLine(line, delimiter, ""); + dataArrayList.add(tempArrayList); + } + } + + /** + * Read a data/column line using the specified delimiter and end tag. + * + * @param line The String line to read. + * @param delimiter The delimiter character. + * @param endTag The token that signifies the end of data/column reading. + */ + private ArrayList readDataLine(String line, char delimiter, String endTag) { + ArrayList returnArrayList = new ArrayList(); + int startIndex = 0; + boolean isFirstToken = true; + do { + // get the end index (the next delimiter or the end of the line) + int endIndex = line.indexOf(delimiter, startIndex); + if (endIndex < 0) { + endIndex = line.length(); + } + + // get the token (space between start and delimiter/end of line) + String token = line.substring(startIndex, endIndex); + + // unquote tokens wrapped in quotes + if (token.startsWith("\"")) { + token = token.substring(1, token.length() - 1); + } + + // if the end token, break out of the loop + if (token.equals(endTag)) { + break; + } + + // skip the first or token + if (!isFirstToken) { + returnArrayList.add(token); + } + isFirstToken = false; + + // check for the end of the line or the end tag + startIndex = endIndex + 1; + if (endIndex >= line.length()) { + break; + } + } + while (true); + return returnArrayList; + } + + /** + * Get a String array of the column names. Returns an empty String array + * if no column names were read. + * + * @return A String array of the column names or an empty String array if + * no column names were read. + */ + public String[] getColumns() { + return (String[]) getColumnsAsList().toArray(new String[columnArrayList.size()]); + } + + /** + * Get a List of the column names. Returns an empty list if no column + * names were read. + * + * @return A List of the column names or an empty list if no column + * names were read. + */ + public List getColumnsAsList() { + List returnList = columnArrayList; + if (returnList == null) { + returnList = new ArrayList(); + } + return returnList; + } + + /** + * Get the number of data rows. + * + * @return The number of data rows. + */ + public int getDataRowCount() { + return dataArrayList.size(); + } + + /** + * Get a String array of data for a particular row. + * + * @param row The row number for which to get the data. + * @return A String array of data for a particular row. + */ + public String[] getDataForRow(int row) { + List rowList = getDataForRowAsList(row); + return (String[]) rowList.toArray(new String[rowList.size()]); + } + + /** + * Get a List of data for a particular row. + * + * @param row The row number for which to get the data. + * @return A List of data for a particular row. + */ + public List getDataForRowAsList(int row) { + return (List) dataArrayList.get(row); + } + + /** + * Get a sublist of the row data. + * + * @param startRow + * @param endRow + * @return A List of List objects of the requested row data. + */ + public List getDataSublist(int startRow, int endRow) { + return dataArrayList.subList(startRow, endRow); + } + + /** + * Get a String array of row data for a specific column name. Returns + * an empty array if the column is not found. + * + * @param columnName The column name for which to fetch data. + * @return A String array of the row data or an empty array if the column + * is not found. + */ + public String[] getDataForColumn(String columnName) { + List dataList = getDataForColumnAsList(columnName); + return (String[]) (dataList.toArray(new String[dataList.size()])); + } + + /** + * Get a List of row data for a specific column name. Returns an + * empty list if the column is not found. + * + * @param columnName The column name for which to fetch data. + * @return A List of the row data or an empty List if the column + * is not found. + */ + public List getDataForColumnAsList(String columnName) { + ArrayList returnList = new ArrayList(); + if (columnArrayList != null) { + int columnNumber = columnArrayList.indexOf(columnName); + if (columnNumber >= 0) { + Iterator iterator = dataArrayList.iterator(); + while (iterator.hasNext()) { + List rowList = (List) iterator.next(); + if (rowList.size() > columnNumber) { + returnList.add(rowList.get(columnNumber)); + } else { + returnList.add(""); + } + } + } + } + return returnList; + } + + /** + * Get a String representation of this CompactFormatData. + * + * @return A String representation of this CompactFormatData. + */ + public String toString() { + StringBuffer stringBuffer = new StringBuffer("CompactFormatData: Columns "); + if (columnArrayList == null) { + stringBuffer.append("(none)"); + } else { + stringBuffer.append(columnArrayList); + } + stringBuffer.append(" Data "); + stringBuffer.append(dataArrayList); + return stringBuffer.toString(); + } + + public static void main(String[] args) { + try { + CompactFormatData cfd = parse(new FileReader("C:\\downloads\\cf.txt")); + System.err.println(cfd); + } catch (IOException e) { + e.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/CompactParser.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/CompactParser.java new file mode 100644 index 0000000..c97fcec --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/CompactParser.java @@ -0,0 +1,208 @@ +/* $Header: /usr/local/cvsroot/rets/commons/src/main/java/org/realtor/rets/util/CompactParser.java,v 1.2 2003/12/04 15:27:03 rsegelman Exp $ */ +package com.ossez.usreio.common.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.Vector; + + +/** + * CompactParser.java Created Aug 1, 2003 + * This is a lightweight parser for RETS compact messages. It scans the compact message and loads + * the keys and values into its internal data structure. The data can then be iterated over by calling the + * nextColumn() method. + * + * + * Copyright 2003, Avantia inc. + * @version $Revision: 1.2 $ + * @author scohen + */ +public class CompactParser { + private String delimiter = "\t"; + private LinkedHashMap map; + private Iterator iter = null; + + public CompactParser() { + map = new LinkedHashMap(); + } + + public CompactParser(String rawXML) { + this(); + parse(rawXML); + } + + public CompactParser(String rawCompact, String metadataXML) { + this(); + parse(rawCompact); + System.out.println("Metadata:\n" + metadataXML); + } + + /** + * resets the iterator to start at the first column + * + */ + public void reset() { + ArrayList toSort = new ArrayList(); + toSort.addAll(map.keySet()); + Collections.sort(toSort); + iter = toSort.iterator(); + } + + /** + * Returns a Vector conaining the names of the columns. + * @return The names of the columns. + */ + public Vector getColumns() { + Vector rv = new Vector(); + Set s = map.keySet(); + + rv.add(s); + + return rv; + } + + /** + * Tests whether or not there are more columns in the iteration. + * @return true if there are more columns waiting, false if there aren't. + */ + public boolean hasMoreColumns() { + if (iter == null) { + reset(); + } + + return iter.hasNext(); + } + + /** + * Get the next column in the sequence. + * @return A string whose value is the name of the next column, or null if there aren't any more columns. + */ + public String nextColumn() { + String rv = null; + + if (iter == null) { + reset(); + } + + if (iter.hasNext()) { + rv = (String) iter.next(); + } else { + iter = null; + } + + return rv; + } + + /** + * Returns the data contained in the column name represented by the String key + * @param key The name of the column whose data you wish to examine. + * @return The data contained in the column, or null if no data is present. + */ + public String getData(String key) { + String rv = null; + + if (map.containsKey(key)) { + rv = (String) map.get(key); + } + + return rv; + } + + private void parse(String xml) { + String tag = getTag("RETS", xml); + System.out.println("first tag is " + tag); + + String delimTag = getTag("DELIMITER", xml); + String delim = getAttribute("value", delimTag); + + if (delim != null) { + delimiter = String.valueOf((char) Integer.parseInt(delim)); + } + + map(getTagBody("COLUMNS", xml), getTagBody("DATA", xml)); + } + + private String getTag(String tagName, String xml) { + int tagStart = xml.indexOf("<" + tagName); + int tagEnd = xml.indexOf(">", tagStart) + 1; + + return xml.substring(tagStart, tagEnd); + } + + private String getTagBody(String tagName, String xml) { + int tagStart = xml.indexOf(">", xml.indexOf(tagName)) + 1; + int tagEnd = xml.indexOf("=/"; + StringTokenizer st = new StringTokenizer(tag.substring(tag.indexOf( + attributeName)), delims, false); + + while (st.hasMoreTokens()) { + String next = st.nextToken(); + + if (next.equals(attributeName)) { + // the next value is the quoted value + String value = st.nextToken(); + + return value.substring(1, value.trim().length() - 1); + } + } + + return null; + } + + private void map(String columns, String data) { + String[] colArr = columns.split(delimiter); + String[] dataArr = data.split(delimiter); + + for (int i = 0; i < colArr.length; i++) { + if ((colArr[i] != null) && (colArr[i].length() > 0)) { + if (dataArr.length > i) { + map.put(colArr[i], DigestUtil.removeQuotes(dataArr[i])); + } else { + map.put(colArr[i], ""); + } + } + } + + System.out.println(map); + } + + public Map getMapping() { + return map; + } + + public static CompactParser getTestInstance() { + String xml = + "" + + " ListOfficeOfficeID ListAgentPager ListOfficeEmail ListAgentPostalCode Style DaysOnMarket LotSizeArea Roof City CoveredParking ListAgentFax ListingStatus ListOfficePostalCode ListAgentOfficePhone StreetNumber PictureData TaxID BathsHalf ListOfficeOfficePhone ListOfficeStreetAdditionalInfo LivingRoomDim PostalCode LivingRoom State ListType Utilities ListingArea Beds Zoning ShowingInstructions ListAgentEmail Heating Cooling Exterior AssociationFee ListAgentCellPhone FamilyRoom ListAgentHomePhone Longitude ListPrice ListingID Baths StreetDirPrefix PublicRemarks ListAgentAgentID ListAgentLastName ExpirationData FamilyRoomDim ClosePrice ListAgentFirstName County Garage LivingArea SaleAgentAgentID ListOfficeFax ListOfficeCity LotSizeDim BoxNumber StatusChangeDate StreetName Basement ListAgentStreetAdditionalInfo ListAgentNRDSMemberID Fireplaces ListAgentCity ListDate Stories ListOfficeStreetName ListOfficeWWW Sundivision OriginalListPrice SaleOfficeOfficeID Directions ListOfficeListingServiceName Remarks DiningRoomDim TaxLegalDescription ListAgentWWW ListAgentStreetName SchoolDistrict ListOfficeState ListAgentState Latitude MapCoordinate CloseDate DiningRoom BathsFull TotalRooms " + + " 2309 44124 2 860 G B CLEVELAND 3 act 44124 (216) 226-4352 3340 1606093 0 (440) 449-2300 12X14 44102 K OH ers 0.00 105.00 4 Call ListAgt nverikakis@core.com \"2,3,B,C\" \"A,C\" 0.00 -81.72779900 74500.00 1004685 2 W 362265 Verikakis 0000-00-00 0.00 Nicholas CUY \"1,B,E\" 1542.00 (440) 449-2574 Mayfield Heights 35X125 0000-00-00 61 ST 0 Mayfield Heights 2002-08-08 2 6030 Mayfield Road #1 74900.00 BETWEEN STORER and CLARK \"Jennie Chiccola Realty, Inc.\" \"JUST RENOVATED*NEW DRIVE,ROOF,GAR.VINYL+NEW ROOF,FURNACE,HWT KITCHEN,2BATHS,CARPETS,ELECTR,ALL COPPER PLMB and DRAINS PVC, MOST DRYWALL NEW,PORCHES,FENCE,BASMT CONC.FLOOR,CEDAR CLOSET UP*OFF KITCHEN 1ST FLOOR LAUNDRY and REAR DECK*ROOM SIZES ARE APPROXIMATE*\" 00X00 6030 Mayfield Road #1 1809 OH OH 41.46359100 D1 0000-00-00 K 2 7 "; + String shortTest = " this is a test"; + CompactParser cp = new CompactParser(xml); + + return cp; + } + + public static void main(String[] args) { + CompactParser cp = CompactParser.getTestInstance(); + + while (cp.hasMoreColumns()) { + String col = cp.nextColumn(); + System.out.println("Mapping: " + col + " : " + cp.getData(col)); + } + + System.out.println("Hey!"); + cp.reset(); + System.out.println(cp.nextColumn()); + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/DesCrypter.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/DesCrypter.java new file mode 100644 index 0000000..41448d8 --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/DesCrypter.java @@ -0,0 +1,92 @@ +package com.ossez.usreio.common.util; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.UnsupportedEncodingException; + +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + + +/** + * + */ +public class DesCrypter { + + private final static Logger logger = LoggerFactory.getLogger(DesCrypter.class); + Cipher ecipher; + Cipher dcipher; + + //String transform = "DES"; + String transform = "DES/ECB/PKCS5Padding"; + + public DesCrypter(byte[] keyBytes) { + SecretKey key = new SecretKeySpec(keyBytes, "DES"); + + try { + ecipher = Cipher.getInstance(transform); + ecipher.init(Cipher.ENCRYPT_MODE, key); + + dcipher = Cipher.getInstance(transform); + dcipher.init(Cipher.DECRYPT_MODE, key); + } catch (javax.crypto.NoSuchPaddingException e) { + e.printStackTrace(); + } catch (java.security.NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (java.security.InvalidKeyException e) { + e.printStackTrace(); + } + } + + public String encrypt(String str) { + try { + // Encode the string into bytes using utf-8 + byte[] utf8 = str.getBytes("UTF8"); + + // Encrypt + byte[] enc = ecipher.doFinal(utf8); + + // Encode bytes to base64 to get a string + + + return Base64.getEncoder().encodeToString(enc); + } catch (javax.crypto.BadPaddingException e) { + e.printStackTrace(); + } catch (IllegalBlockSizeException e) { + e.printStackTrace(); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return null; + } + + public String decrypt(String str) { + try { + // Decode base64 to get bytes + byte[] dec = Base64.getDecoder().decode(str); + + // Decrypt + byte[] utf8 = dcipher.doFinal(dec); + + // Decode using utf-8 + return new String(utf8, "UTF8"); + } catch (javax.crypto.BadPaddingException e) { + e.printStackTrace(); + } catch (IllegalBlockSizeException e) { + e.printStackTrace(); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (java.io.IOException e) { + e.printStackTrace(); + } + + return null; + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/DigestUtil.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/DigestUtil.java new file mode 100644 index 0000000..826a1c1 --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/DigestUtil.java @@ -0,0 +1,150 @@ +/* + * DigestUtil.java + * + * Created on October 4, 2002 + */ +package com.ossez.usreio.common.util; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.*; + +import java.util.*; + + +/** + * @version 1.0 + */ +public class DigestUtil { + /** + * log4j Category object + */ + private final static Logger logger = LoggerFactory.getLogger(DigestUtil.class); + + /** + * Create digest Authentication String + */ + public static String digestAuthorization(String username, String password, + String method, String uri, String value) { + Map m = parseAuthenticate(value); + String realm = (String) m.get("realm"); + String nonce = (String) m.get("nonce"); + String opaque = (String) m.get("opaque"); + String nc = "00000001"; + String cnonce = (String) m.get("cnonce"); + String qop = (String) m.get("qop"); + + boolean isRFC2617 = (qop != null); // Recognizes differences between RFC2069 and RFC2617 + + String digestResponse = Digest(username, realm, password, method, uri, + nonce, nc, cnonce, qop); + + String digest = "Digest username=\"" + username + "\", " + "realm=\"" + + realm + "\", " + "nonce=\"" + nonce + "\", " + "opaque=\"" + + opaque + "\", " + "uri=\"" + uri + "\", " + "response=\"" + + digestResponse + "\""; + + if (isRFC2617) { + digest += (", qop=\"" + qop + "\", " + "cnonce=\"" + cnonce + + "\", " + "nc=\"" + nc + "\""); + } + + return digest; + } + + public static Map parseAuthenticate(String s) { + String key = null; + String value = null; + int equalSign = 0; + HashMap map = new HashMap(); + StringTokenizer commaTokenizer = new StringTokenizer(s, ","); + + while (commaTokenizer.hasMoreTokens()) { + String token = commaTokenizer.nextToken(); + + if ((equalSign = token.indexOf("=")) >= 0) { + key = token.substring(0, equalSign).trim(); + value = removeQuotes(token.substring(equalSign + 1).trim()); + map.put(key, value); + + //cat.debug("parseAuthenticate:["+key+"]=["+value+"]"); + } + } + + return map; + } + + public static String Digest(String username, String realm, String password, + String method, String uri, String nonce, String nc, String cnonce, + String qop) { + String digestResponse = null; + boolean isRFC2617 = (qop != null); // Recognizes differences between RFC2069 and RFC2617 + + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + + md.reset(); + + String a1 = username + ":" + realm + ":" + password; + + String digestA1 = HexUtils.convert(md.digest(a1.getBytes())); + logger.debug("evaluateMD5ResponseDigest: digestA1(" + a1 + ")=" + + digestA1); + + md.reset(); + + String a2 = method + ":" + uri; + String digestA2 = HexUtils.convert(md.digest(a2.getBytes())); + logger.debug("evaluateMD5ResponseDigest: digestA2(" + a2 + ")=" + + digestA2); + + md.reset(); + + String response = digestA1 + ":" + nonce; + + if (isRFC2617) { + response += (":" + nc + ":" + cnonce + ":" + qop); + } + + response += (":" + digestA2); + + digestResponse = HexUtils.convert(md.digest(response.getBytes())); + logger.debug("evaluateMD5ResponseDigest: digestResponse(" + response + + ")=" + digestResponse); + + return digestResponse; + } catch (Exception e) { + //cat.error("evaluateMD5ResponseDigest: Exception occurred!",e); + return digestResponse; + } + } + + public static String Digest(String value) { + String digestResponse = null; + + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + + md.reset(); + + digestResponse = HexUtils.convert(md.digest(value.getBytes())); + + md.reset(); + } catch (Exception e) { + // cat.error("evaluateMD5ResponseDigest: Exception occurred!",e); + } + + return digestResponse; + } + + /** + * Removes the quotes on a string. + */ + public static String removeQuotes(String quotedString) { + quotedString = quotedString.replace('\"', ' '); + + return quotedString.trim(); + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/HexUtils.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/HexUtils.java new file mode 100644 index 0000000..8d6846d --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/HexUtils.java @@ -0,0 +1,88 @@ +package com.ossez.usreio.common.util; + +import java.io.*; + + +public final class HexUtils { + /** + * Convert a String of hexadecimal digits into the corresponding + * byte array by encoding each two hexadecimal digits as a byte. + * + * @param digits Hexadecimal digits representation + * + * @exception IllegalArgumentException if an invalid hexadecimal digit + * is found, or the input string contains an odd number of hexadecimal + * digits + */ + public static byte[] convert(String digits) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + for (int i = 0; i < digits.length(); i += 2) { + char c1 = digits.charAt(i); + + if ((i + 1) >= digits.length()) { + throw new IllegalArgumentException("Must be a hex number"); + } + + char c2 = digits.charAt(i + 1); + byte b = 0; + + if ((c1 >= '0') && (c1 <= '9')) { + b += ((c1 - '0') * 16); + } else if ((c1 >= 'a') && (c1 <= 'f')) { + b += ((c1 - 'a' + 10) * 16); + } else if ((c1 >= 'A') && (c1 <= 'F')) { + b += ((c1 - 'A' + 10) * 16); + } else { + throw new IllegalArgumentException("Must be a hex number"); + } + + if ((c2 >= '0') && (c2 <= '9')) { + b += (c2 - '0'); + } else if ((c2 >= 'a') && (c2 <= 'f')) { + b += (c2 - 'a' + 10); + } else if ((c2 >= 'A') && (c2 <= 'F')) { + b += (c2 - 'A' + 10); + } else { + throw new IllegalArgumentException("Must be a hex number"); + } + + baos.write(b); + } + + return (baos.toByteArray()); + } + + /** + * Convert a byte array into a printable format containing a + * String of hexadecimal digit characters (two per byte). + * + * @param bytes Byte array representation + */ + public static String convert(byte[] bytes) { + StringBuffer sb = new StringBuffer(bytes.length * 2); + + for (int i = 0; i < bytes.length; i++) { + sb.append(convertDigit((int) (bytes[i] >> 4))); + sb.append(convertDigit((int) (bytes[i] & 0x0f))); + } + + return (sb.toString()); + } + + /** + * [Private] Convert the specified value (0 .. 15) to the corresponding + * hexadecimal digit. + * + * @param value Value to be converted + */ + private static char convertDigit(int value) { + value &= 0x0f; + + if (value >= 10) { + return ((char) (value - 10 + 'a')); + } else { + return ((char) (value + '0')); + } + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/MD5Util.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/MD5Util.java new file mode 100644 index 0000000..acefb3f --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/MD5Util.java @@ -0,0 +1,68 @@ +/* + * MD5Util.java + * + * Created on November 21, 2001, 9:54 AM + */ +package com.ossez.usreio.common.util; + +import java.security.*; + + +/** + * + * @version 1.0 + */ +public class MD5Util { + /** Creates new MD5Util */ + public MD5Util() { + } + + /** returns MD5 HEX value for a given string + * @param source string to convert + */ + public static String getDigestAsHexString(String source) { + return getDigestAsHexString(source.getBytes()); + } + + /** returns MD5 HEX value for a given a byte array + * @param source byte array to convert + */ + public static String getDigestAsHexString(byte[] source) { + String retStr = null; + + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.reset(); + + byte[] digestBytes = md.digest(source); + retStr = convertToHex(digestBytes); + } catch (java.security.NoSuchAlgorithmException nsa) { + nsa.printStackTrace(); + } + + return retStr; + } + + /** Returns the HEX representation of a byte array + * @param source byte array to convert to HEX + */ + private static String convertToHex(byte[] source) { + String text; + int j; + int i; + text = ""; + + for (i = 0; i < 16; i++) { + j = source[i]; + j = j & 255; + + if (j <= 15) { + text += "0"; + } + + text += Integer.toHexString(j); + } + + return text; + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/PropertiesLocator.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/PropertiesLocator.java new file mode 100644 index 0000000..0a106ca --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/PropertiesLocator.java @@ -0,0 +1,34 @@ +/* $Header: /usr/local/cvsroot/rets/commons/src/main/java/org/realtor/rets/util/PropertiesLocator.java,v 1.2 2003/12/04 15:27:03 rsegelman Exp $ */ +package com.ossez.usreio.common.util; + +import java.io.IOException; + +import java.util.Properties; + + +/** + * PropertiesLocator.java Created Aug 6, 2003 + * + * + * Copyright 2003, Avantia inc. + * @version $Revision: 1.2 $ + * @author scohen + */ +public class PropertiesLocator { + public static Properties locateProperties(String fileName) + throws PropertiesNotFoundException { + ClassLoader loader = PropertiesLocator.class.getClassLoader(); + Properties p = new Properties(); + + try { + p.load(loader.getResourceAsStream(fileName)); + } catch (IOException e) { + PropertiesNotFoundException nfe = new PropertiesNotFoundException( + "Could not find file " + fileName); + nfe.fillInStackTrace(); + throw nfe; + } + + return p; + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/PropertiesNotFoundException.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/PropertiesNotFoundException.java new file mode 100644 index 0000000..83dc842 --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/PropertiesNotFoundException.java @@ -0,0 +1,17 @@ +/* $Header: /usr/local/cvsroot/rets/commons/src/main/java/org/realtor/rets/util/PropertiesNotFoundException.java,v 1.2 2003/12/04 15:27:03 rsegelman Exp $ */ +package com.ossez.usreio.common.util; + + +/** + * PropertiesNotFoundException.java Created Aug 6, 2003 + * + * + * Copyright 2003, Avantia inc. + * @version $Revision: 1.2 $ + * @author scohen + */ +public class PropertiesNotFoundException extends Exception { + public PropertiesNotFoundException(String message) { + super(message); + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSCompactHandler.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSCompactHandler.java new file mode 100644 index 0000000..eadf1cc --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSCompactHandler.java @@ -0,0 +1,186 @@ +/* + * RETSCompactHandler.java + * + * Created on October 2, 2002, 8:45 AM + */ +package com.ossez.usreio.common.util; + +import org.xml.sax.*; +import org.xml.sax.helpers.*; + +import java.io.*; + +import java.util.StringTokenizer; + +import javax.xml.parsers.*; + + +//import org.apache.xerces.parsers.SAXParser; +public class RETSCompactHandler extends DefaultHandler { + private OutputStream os = System.out; + private String delim = " "; + private StringBuffer textBuffer; + private String currentElement = null; + + public void setOutputStream(OutputStream p_os) { + this.os = p_os; + } + + private void nl() throws SAXException { + String lineEnd = "\r\n"; + + try { + os.write(lineEnd.getBytes()); + } catch (IOException e) { + throw new SAXException("I/O error", e); + } + } + + private void emit(String s) throws SAXException { + try { + os.write(s.getBytes()); + os.flush(); + } catch (IOException e) { + throw new SAXException("I/O error", e); + } + } + + public void startDocument() throws SAXException { + emit(""); + nl(); + } + + public void endDocument() throws SAXException { + try { + nl(); + os.flush(); + } catch (IOException e) { + throw new SAXException("I/O error", e); + } + } + + public void startElement(String namespaceURI, String sName, // simple name + String qName, // qualified name + Attributes attrs) throws SAXException { + textBuffer = null; + + String eName = qName; // element name + currentElement = qName; + + if (qName.equalsIgnoreCase("DELIMITER")) { + // delimiter is a 2 digit HEX value + String delimOct = attrs.getValue("", "value"); + delim = "" + (char) Integer.parseInt(delimOct, 16); + } + + if ("".equals(eName)) { + eName = qName; // not namespaceAware + } + + emit("<" + eName); + + if (attrs != null) { + for (int i = 0; i < attrs.getLength(); i++) { + String aName = attrs.getLocalName(i); // Attr name + + if ("".equals(aName)) { + aName = attrs.getQName(i); + } + + emit(" "); + emit(aName + "=\"" + attrs.getValue(i) + "\""); + } + } + + emit(">"); + } + + public void endElement(String namespaceURI, String sName, // simple name + String qName // qualified name + ) throws SAXException { + if (textBuffer != null) { + parseText(textBuffer.toString()); + } + + textBuffer = null; + + String eName = qName; // element name + + if ("".equals(eName)) { + eName = qName; // not namespaceAware + } + + emit(""); + nl(); + currentElement = null; + } + + public void characters(char[] buf, int offset, int len) + throws SAXException { + String s = new String(buf, offset, len); + + if (textBuffer == null) { + textBuffer = new StringBuffer(s); + } else { + textBuffer.append(s); + } + } + + public void parseText(String text) throws SAXException { + if (currentElement == null) { + emit(text); + + return; + } + + String eName = "d"; + + if (currentElement.equalsIgnoreCase("COLUMNS")) { + eName = "c"; + } + + String start = "<" + eName + ">"; + String end = ""; + + if (currentElement.equalsIgnoreCase("COLUMNS") || + currentElement.equalsIgnoreCase("DATA")) { + StringTokenizer st = new StringTokenizer(text, delim, true); + boolean firstToken = true; + boolean lastTokenDelim = false; + + while (st.hasMoreTokens()) { + String token = st.nextToken(); + + if (token.equalsIgnoreCase(delim)) { + if (lastTokenDelim) { + emit(start + end); + } + + lastTokenDelim = true; + } else { + emit(start + token + end); + lastTokenDelim = false; + } + } + } else { + emit(text); + } + } + + public static void main(String[] args) { + DefaultHandler h = new RETSCompactHandler(); + + //SAXParser p = new SAXParser(); + try { + SAXParserFactory spf = SAXParserFactory.newInstance(); + SAXParser p = spf.newSAXParser(); + + FileInputStream fis = new FileInputStream("c:/tmp/xx.xml"); + InputSource is = new InputSource(fis); + p.parse(is, h); + fis.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSConfigurator.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSConfigurator.java new file mode 100644 index 0000000..02c27e1 --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSConfigurator.java @@ -0,0 +1,24 @@ +// $Header: /usr/local/cvsroot/rets/commons/src/main/java/org/realtor/rets/util/RETSConfigurator.java,v 1.2 2003/12/04 15:27:03 rsegelman Exp $ +package com.ossez.usreio.common.util; + + + + +/** + * RETSConfigurator + * Singleton to limit number of times BasicConfigurator.configure is called. + */ +public class RETSConfigurator { + static boolean configured = false; + + private RETSConfigurator() { + } + + /** calls BasicConfigurator.configure() only once */ + static public void configure() { + if (!configured) { +// BasicConfigurator.configure(); + configured = true; + } + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSReplyCodes.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSReplyCodes.java new file mode 100644 index 0000000..edea16f --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSReplyCodes.java @@ -0,0 +1,46 @@ +// $Header: /usr/local/cvsroot/rets/commons/src/main/java/org/realtor/rets/util/RETSReplyCodes.java,v 1.4 2005/04/06 12:44:06 ekovach Exp $ +package com.ossez.usreio.common.util; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Properties; + + +/** + * Handles the mapping of replyCodes to replyText + * + * @author $Author: ekovach $ + * @version $Revision: 1.4 $ + */ +public class RETSReplyCodes { + /** + * log4j Category object + */ + private final static Logger logger = LoggerFactory.getLogger(RETSReplyCodes.class); + + /** + * holds the Properties (Codes->Text) that are loaded at startup. + */ + static Properties replyCodesProperties = null; + + static { + try { + replyCodesProperties = PropertiesLocator.locateProperties( + "RETSReplyCodes.properties"); + } catch (Exception e) { + logger.error("Error Loading RETSReplyCodes.properties", e); + } + } + + /** + * get the ReplyText mapped by the ReplyCode + * + * @param code error code + * @return String of text mapped from code + */ + static public String get(int code) { + return replyCodesProperties.getProperty("" + code); + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSRequestResponse.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSRequestResponse.java new file mode 100644 index 0000000..8299f34 --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/RETSRequestResponse.java @@ -0,0 +1,63 @@ +/** + * RETSRequestResponse.java + * + * @author jbrush + * @version + */ +package com.ossez.usreio.common.util; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.util.*; + + +/////////////////////////////////////////////////////////////////////// +public class RETSRequestResponse implements Serializable { + private final static Logger logger = LoggerFactory.getLogger(RETSRequestResponse.class); + private HashMap req = null; + private HashMap resp = null; + + public RETSRequestResponse() { + req = new HashMap(); + resp = new HashMap(); + } + + /////////////////////////////////////////////////////////////////////// + public void setRequestVariable(String key, String value) { + req.put(key, value); + } + + public String getRequestVariable(String key) { + return (String) req.get(key); + } + + public Map getRequestMap() { + return (Map) req; + } + + public void addToRequestMap(Map m) { + req.putAll(m); + } + + /////////////////////////////////////////////////////////////////////// + public void setResponseVariable(String key, String value) { + resp.put(key, value); + } + + public String getResponseVariable(String key) { + return (String) resp.get(key); + } + + public Map getResponseMap() { + return (Map) resp; + } + + public void addToResponseMap(Map m) { + resp.putAll(m); + } + + /////////////////////////////////////////////////////////////////////// +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/Resource.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/Resource.java new file mode 100644 index 0000000..4656889 --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/Resource.java @@ -0,0 +1,214 @@ +package com.ossez.usreio.common.util; + +import java.io.*; + +import java.net.*; + +import java.util.*; + + +/** + * A class to locate resources, retrieve their contents, and determine their + * last modified time. To find the resource the class searches the CLASSPATH + * first, then Resource.class.getResource("/" + name). If the Resource finds + * a "file:" URL then the file path will be treated as a file. Otherwise the + * path is treated as a URL and has limited last modified info. + */ +public class Resource implements Serializable { + private String name; + private File file; + private URL url; + + public Resource(String name) throws IOException { + this.name = name; + + SecurityException exception = null; + + try { + // Search using the CLASSPATH. If found, "file" is set and the call + // returns true. A SecurityException may bubble up. + if (tryClasspath(name)) { + return; + } + } catch (SecurityException e) { + exception = e; // save for later + } + + try { + // Search using the class loader getResource(). If found as a file, + // "file" is set, if found as a URL then "url" is set. + if (tryLoader(name)) { + return; + } + } catch (SecurityException e) { + exception = e; // save for later + } + + // If we get here we failed, report the exception + String msg = ""; + + if (exception != null) { + msg = ": " + exception; + } + + throw new IOException("Resource '" + name + "' could not be found in " + + "the CLASSPATH (" + System.getProperty("java.class.path") + + "), nor could it be located by the classloader responsible for the " + + "web application (WEB-INF/classes)" + msg); + } + + /** + * Returns the resource name, as passed to the constructor. + */ + public String getName() { + return name; + } + + /** + * Returns an input stream to read the resource contents. + */ + public InputStream getInputStream() throws IOException { + if (file != null) { + return new BufferedInputStream(new FileInputStream(file)); + } else if (url != null) { + return new BufferedInputStream(url.openStream()); + } + + return null; + } + + /** + * Returns when the resource was last modified. If the resource was found + * using a URL, this method will only work if the URL connection supports + * last modified information. If there's no support, Long.MAX_VALUE is + * returned. Perhaps this should return -1 but we return MAX_VALUE on + * the assumption that if you can't determine the time, assume it's + * maximally new. + */ + public long lastModified() { + if (file != null) { + return file.lastModified(); + } else if (url != null) { + try { + return url.openConnection().getLastModified(); // hail mary + } catch (IOException e) { + return Long.MAX_VALUE; + } + } + + return 0; // can't happen + } + + /** + * Returns the directory containing the resource, or null if the resource + * isn't directly available on the filesystem. This value can be used to + * locate the config file on disk or write files in the same directory. + */ + public String getDirectory() { + if (file != null) { + return file.getParent(); + } else if (url != null) { + return null; + } + + return null; + } + + // Returns true if found + private boolean tryClasspath(String filename) { + String classpath = System.getProperty("java.class.path"); + String[] paths = split(classpath, File.pathSeparator); + file = searchDirectories(paths, filename); + + return (file != null); + } + + private static File searchDirectories(String[] paths, String filename) { + SecurityException exception = null; + + for (int i = 0; i < paths.length; i++) { + try { + File file = new File(paths[i], filename); + + if (file.exists() && !file.isDirectory()) { + return file; + } + } catch (SecurityException e) { + // Security exceptions can usually be ignored, but if all attempts + // to find the file fail, then report the (last) security exception. + exception = e; + } + } + + // Couldn't find any match + if (exception != null) { + throw exception; + } else { + return null; + } + } + + // Splits a String into pieces according to a delimiter. + // Uses JDK 1.1 classes for backward compatibility. + // JDK 1.4 actually has a split() method now. + private static String[] split(String str, String delim) { + // Use a Vector to hold the splittee strings + Vector v = new Vector(); + + // Use a StringTokenizer to do the splitting + StringTokenizer tokenizer = new StringTokenizer(str, delim); + + while (tokenizer.hasMoreTokens()) { + v.addElement(tokenizer.nextToken()); + } + + String[] ret = new String[v.size()]; + v.copyInto(ret); + + return ret; + } + + // Returns true if found + private boolean tryLoader(String name) { + name = "/" + name; + + URL res = Resource.class.getResource(name); + + if (res == null) { + return false; + } + + // Try converting from a URL to a File + File resFile = urlToFile(res); + + if (resFile != null) { + file = resFile; + } else { + url = res; + } + + return true; + } + + private static File urlToFile(URL res) { + String externalForm = res.toExternalForm(); + + if (externalForm.startsWith("file:")) { + return new File(externalForm.substring(5)); + } + + return null; + } + + public String toString() { + return "[Resource: File: " + file + " URL: " + url + "]"; + } + + public URL toURL() throws MalformedURLException { + if (file != null) { + return file.toURL(); + } else { + return url; + } + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/ResourceLocator.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/ResourceLocator.java new file mode 100644 index 0000000..3a7696e --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/ResourceLocator.java @@ -0,0 +1,58 @@ +/* + * Created on Nov 19, 2003 + * + * To change the template for this generated file go to + * Window>Preferences>Java>Code Generation>Code and Comments + * + + * $Source: /usr/local/cvsroot/rets/commons/src/main/java/org/realtor/rets/util/ResourceLocator.java,v $ + + * $Date: 2003/11/21 16:16:08 $ + + * $Revision: 1.1.1.1 $ + + * + + ******************************************************************************* + +*/ +package com.ossez.usreio.common.util; + +import java.util.HashMap; +import java.util.Map; + + +/** + * @author rsegelman + * + * To change the template for this generated type comment go to + * {@literal Window > Preferences > Java >Code Generation> Code and Comments} + */ +public class ResourceLocator { + protected static Map map = new HashMap(); + + public static String locate(String resourceKey) { + if (get(resourceKey) == null) { + try { + Resource res = new Resource(resourceKey); + + String defaultSystemId; + defaultSystemId = res.getDirectory() + "/" + res.getName(); + + set(resourceKey, defaultSystemId); + } catch (Exception e) { + e.printStackTrace(); + } + } + + return get(resourceKey); + } + + public static String get(String resourceKey) { + return (String) map.get(resourceKey); + } + + public static void set(String resourceKey, String value) { + map.put(resourceKey, value); + } +} diff --git a/rets-io-common/src/main/java/com/ossez/usreio/common/util/XMLUtils.java b/rets-io-common/src/main/java/com/ossez/usreio/common/util/XMLUtils.java new file mode 100644 index 0000000..9524885 --- /dev/null +++ b/rets-io-common/src/main/java/com/ossez/usreio/common/util/XMLUtils.java @@ -0,0 +1,249 @@ +/* + * XMLUtils.java + * + * Created on December 6, 2001, 1:21 PM + */ +package com.ossez.usreio.common.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.OutputStream; +import java.io.StringReader; +import java.util.Iterator; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.apache.xpath.XPathAPI; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + + +/** + * Utilities for dealing with XML + * @author tweber + * @version 1.0 + */ +public class XMLUtils { + /** This method dumps out a dom document to an output stream. + * Header information is turned off. + * + * @param doc Dom Document + * @param os existing outputstream you wish to write the document to. + */ + public static void DOMtoOutputStream(Document doc, OutputStream os) { + try { + TransformerFactory tFactory = TransformerFactory.newInstance(); + Transformer transformer = tFactory.newTransformer(); + DOMSource src = new DOMSource(doc); + StreamResult result = new StreamResult(os); + + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + + //transformer.setOutputProperty(OutputKeys.METHOD, "xml"); + transformer.transform(src, result); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** Does an xsl tranformation of an XML document and returns a String result. + * + * @param xmlDoc string value of an xmlDocument + * @param xslFile filename of an XSL file. + */ + public static String transformXmlToString(String xmlDoc, String xslFile, + Map parameters) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transformXml(xmlDoc, xslFile, baos, parameters); + + return baos.toString(); + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + public static String transformXmlToString(String xmlDoc, String xslFile) { + return transformXmlToString(xmlDoc, xslFile, null); + } + + public static void transformXml(String xmlDoc, String xslFile, + OutputStream os) { + transformXml(xmlDoc, xslFile, os, null); + } + + /** Does an xsl tranformation of an XML document and writes the result. + * to an output stream. + * + * @param xmlDoc string value of an xmlDocument + * @param xslFile filename of an XSL file. + * @param os OutputStream to write the results to. + */ + public static void transformXml(String xmlDoc, String xslFile, + OutputStream os, Map parameters) { + try { + File stylesheet = new File(xslFile); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + ByteArrayInputStream bais = new ByteArrayInputStream(xmlDoc.getBytes()); + + String urlString= "file:"+ResourceLocator.locate("dummy.dtd"); + System.out.println("URL String:"+urlString); + Document document = builder.parse(bais,urlString); + + // Use a Transformer for output + TransformerFactory tFactory = TransformerFactory.newInstance(); + StreamSource stylesource = new StreamSource(stylesheet); + Transformer transformer = tFactory.newTransformer(stylesource); + + DOMSource source = new DOMSource(document); + StreamResult result = new StreamResult(os); + + if (parameters != null) { + Iterator iter = parameters.keySet().iterator(); + + while (iter.hasNext()) { + String key = (String) iter.next(); + transformer.setParameter(key, parameters.get(key)); + } + } + + transformer.transform(source, result); + } catch (TransformerConfigurationException tce) { + // Error generated by the parser + System.out.println("\n** Transformer Factory error"); + System.out.println(" " + tce.getMessage()); + + // Use the contained exception, if any + Throwable x = tce; + + if (tce.getException() != null) { + x = tce.getException(); + } + + x.printStackTrace(); + } catch (TransformerException te) { + // Error generated by the parser + System.out.println("\n** Transformation error"); + System.out.println(" " + te.getMessage()); + + // Use the contained exception, if any + Throwable x = te; + + if (te.getException() != null) { + x = te.getException(); + } + + x.printStackTrace(); + } catch (Exception e) { + // error + e.printStackTrace(); + } + } + + /** Creates an Element and sets the text value of the element. Appends the + * element to rootNode. + * + * @param doc DOM Document + * @param rootNode node to add an element to + * @param elementName name of the new element + * @param elementValue value of the new element. + */ + public static void addTextElement2Node(Document doc, Node rootNode, + String elementName, String elementValue) { + try { + Element element = doc.createElement(elementName); + Node text = doc.createTextNode(elementValue); + element.appendChild(text); + + //doc.appendChild(element); + rootNode.appendChild(element); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static NodeList executeXpathQuery(Node root, String query) + throws TransformerException { + return XPathAPI.selectNodeList(root, query); + } + + public static void printNodeList(NodeList list) { + for (int i = 0; i < list.getLength(); i++) { + printNode(list.item(i), ""); + System.out.println("############################"); + } + } + + public static void printNode(Node node, String indent) { + String nodeName = node.getNodeName(); + System.out.print(indent); + + if (nodeName.equals("#text")) { + String nodeText = node.getNodeValue(); + + if ((nodeText != null) && (nodeText.trim().length() > 0)) { + System.out.print(nodeText.trim()); + } + } else { + System.out.print(nodeName); + } + + System.out.print(" "); + + if (!nodeName.equals("#text")) { + NamedNodeMap attrs = node.getAttributes(); + + if (attrs != null) { + for (int i = 0; i < attrs.getLength(); i++) { + Node attr = attrs.item(i); + System.out.print(attr.getNodeName() + "=\"" + + attr.getNodeValue() + "\" "); + } + } + } + + NodeList children = node.getChildNodes(); + + for (int i = 0; i < children.getLength(); i++) { + System.out.println(); + printNode(children.item(i), indent + "\t"); + } + } + + public static Document stringToDocument(String xml) { + Document doc = null; + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + + // Turn on validation, and turn off namespaces + factory.setValidating(false); + factory.setNamespaceAware(false); + + try { + DocumentBuilder builder = factory.newDocumentBuilder(); + doc = builder.parse(new InputSource(new StringReader(xml))); + } catch (Exception e) { + e.printStackTrace(); + } + + return doc; + } +} diff --git a/rets-io-common/src/main/resources/log4j.properties b/rets-io-common/src/main/resources/log4j.properties new file mode 100644 index 0000000..a4e6cd2 --- /dev/null +++ b/rets-io-common/src/main/resources/log4j.properties @@ -0,0 +1,38 @@ +log4j.rootCategory=error, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n + + +# client API logfile +log4j.appender.R1=org.apache.log4j.RollingFileAppender +log4j.appender.R1.File=/tmp/retsClientAPI.log +log4j.appender.R1.MaxFileSize=100KB +log4j.appender.R1.MaxBackupIndex=3 +log4j.appender.R1.layout=org.apache.log4j.PatternLayout +log4j.appender.R1.layout.ConversionPattern=%p %t %c CLIENTAPI- %m%n + + +# server logfile +log4j.appender.R2=org.apache.log4j.RollingFileAppender +log4j.appender.R2.File=/tmp/retsServer.log +log4j.appender.R2.MaxFileSize=100KB +log4j.appender.R2.MaxBackupIndex=3 +log4j.appender.R2.layout=org.apache.log4j.PatternLayout +log4j.appender.R2.layout.ConversionPattern=%p %t %c SERVER - %m%n + +# setup RETS client API logging +log4j.category.org.realtor.rets.retsapi=debug, R1 +log4j.category.org.realtor.rets.util=debug, R1 + + +# setup RETS server logging +log4j.category.org.realtor.rets.server=debug, R2, stdout +log4j.category.org.realtor.rets.util=debug, R2 +log4j.category.org.realtor.rets.persistance=error, R2 + + +log4j.additivity.org.realtor.rets.server=false +log4j.additivity.org.realtor.rets.retsapi=false +log4j.additivity.org.realtor.rets.util=false