├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── deploy.yml ├── .gitignore ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle ├── durian-rx.png ├── durian.svg ├── durian.svg.license ├── gradle.properties ├── gradle ├── spotless.eclipseformat.xml ├── spotless.importorder ├── spotless.license.java └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── com │ └── diffplug │ └── common │ └── rx │ ├── CasBox.java │ ├── CasBoxImp.java │ ├── Chit.java │ ├── ChitImpl.java │ ├── ForwardingBox.kt │ ├── GuardedExecutor.kt │ ├── IFlowable.java │ ├── LockBox.java │ ├── LockBoxImp.java │ ├── MappedImp.kt │ ├── MultiSelectModel.kt │ ├── OrderedLock.java │ ├── Rx.kt │ ├── RxBox.kt │ ├── RxBoxImp.kt │ ├── RxExecutor.kt │ ├── RxGetter.kt │ ├── RxListener.kt │ ├── RxLockBox.kt │ ├── RxLockBoxImp.kt │ ├── RxOrderedSet.java │ ├── RxSubscriber.kt │ ├── RxTracingPolicy.kt │ ├── StackDumper.java │ └── package-info.java └── test └── java └── com └── diffplug └── common └── rx ├── CasBoxTest.java ├── ChitTest.java ├── LockBoxTest.java ├── OrderedLockTest.java ├── PackageSanityTests.java ├── RxAndListenableFutureSemantics.java ├── RxApiJustification.java ├── RxAsserter.java ├── RxBoxTest.java ├── RxExample.java ├── RxGetterTest.java ├── RxLockBoxTest.java └── RxOrderedSetTest.java /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.MF text eol=crlf 3 | *.exe binary 4 | *.pfx binary 5 | *.tar binary 6 | *.docx binary 7 | *.jar binary 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | jobs: 9 | build: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | jre: [17] 14 | os: [ubuntu-latest, windows-latest] 15 | include: 16 | - jre: 21 17 | os: ubuntu-latest 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | - name: Install JDK ${{ matrix.jre }} 23 | uses: actions/setup-java@v4 24 | with: 25 | distribution: "temurin" 26 | java-version: ${{ matrix.jre }} 27 | - name: gradle caching 28 | uses: gradle/actions/setup-gradle@v4 29 | - run: git fetch origin main 30 | - run: ./gradlew build --no-configuration-cache 31 | - name: junit result 32 | uses: mikepenz/action-junit-report@v4 33 | if: always() # always run even if the previous step fails 34 | with: 35 | check_name: JUnit ${{ matrix.jre }} ${{ matrix.os }} 36 | report_paths: "build/test-results/*/TEST-*.xml" 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # NEXUS_USER 2 | # NEXUS_PASS64 (base64 NOTE: `base64` and `openssl base64` failed, had to use Java 3 | # byte[] data = "{{password}}".getBytes(StandardCharsets.UTF_8); 4 | # String encoded = new String(Base64.getEncoder().encode(data), StandardCharsets.UTF_8); 5 | # System.out.println(encoded); 6 | # GPG_PASSPHRASE 7 | # GPG_KEY64 (base64) 8 | # gpg --export-secret-keys --armor KEY_ID | openssl base64 | pbcopy 9 | 10 | on: 11 | workflow_dispatch: 12 | inputs: 13 | to_publish: 14 | description: 'What to publish' 15 | required: true 16 | default: 'all' 17 | type: choice 18 | options: 19 | - all 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | name: deploy 24 | env: 25 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | ORG_GRADLE_PROJECT_nexus_user: ${{ secrets.NEXUS_USER }} 27 | ORG_GRADLE_PROJECT_nexus_pass64: ${{ secrets.NEXUS_PASS64 }} 28 | ORG_GRADLE_PROJECT_gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} 29 | ORG_GRADLE_PROJECT_gpg_key64: ${{ secrets.GPG_KEY64 }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: jdk 17 33 | uses: actions/setup-java@v4 34 | with: 35 | java-version: 17 36 | distribution: 'temurin' 37 | - name: gradle caching 38 | uses: gradle/actions/setup-gradle@v4 39 | - name: git fetch origin main 40 | run: git fetch origin main 41 | - name: publish all 42 | if: "${{ github.event.inputs.to_publish == 'all' }}" 43 | run: | 44 | ./gradlew :changelogPush -Prelease=true -Penable_publishing=true --stacktrace --warning-mode all --no-configuration-cache 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ stuff 2 | .idea/ 3 | 4 | # mac stuff 5 | *.DS_Store 6 | 7 | # gradle stuff 8 | .gradle/ 9 | build/ 10 | 11 | # Eclipse stuff 12 | .project 13 | .classpath 14 | .settings/ 15 | bin/ 16 | 17 | # manifest.mf 18 | MANIFEST.MF 19 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # DurianRx releases 2 | 3 | ## [Unreleased] 4 | 5 | ## [5.1.0] - 2025-02-01 6 | ### Added 7 | - `RxExecutor.launch` lets a user run `suspend` functions within that executor. 8 | - `GuardedExecutor` now has a lazily populated scope which cancels when its guard disposes, as well as a `launch` method. 9 | 10 | ## [5.0.2] - 2025-01-31 11 | ### Fixed 12 | - `Rx.createEmitFlow()` now creates a flow with unlimited buffer, same as RxJava's `PublishSubject`. 13 | 14 | ## [5.0.1] - 2025-01-26 15 | ### Changed 16 | - Downgrade from Kotlin `2.1.0` to `2.0.21` 17 | 18 | ## [5.0.0] - 2025-01-24 19 | ### Changed 20 | - **BREAKING** Replace `RxJava Disposable` with `Kotlin Job`, and remove `rxjava` completely. ([#10](https://github.com/diffplug/durian-rx/pull/10)) 21 | - Add strict nullability to RxBox and improve efficiency. ([#12](https://github.com/diffplug/durian-rx/pull/12)) 22 | - Bump required java from 11 to 17. ([#9](https://github.com/diffplug/durian-rx/pull/9)) 23 | 24 | ## [4.0.1] - 2022-12-20 25 | ### Fixed 26 | - Generics on `MultiSelectModel` have changed from `T` to `T : Any` to play nicely with Kotlin's new stricter generic nullability bounds. ([#8](https://github.com/diffplug/durian-rx/pull/8)) 27 | 28 | ## [4.0.0] - 2022-09-29 29 | ### Added 30 | * Add `merge` function to `MultiSelectModel.Trumped` 31 | * `RxExecutor` now includes a `dispatcher: CoroutineDispatcher` field 32 | ### Changed (important) 33 | * **BREAKING** `RxBox`, `RxGetter`, and `IObservable` are now based on kotlin `Flow` rather than `RxJava`. 34 | ### Changed (but probably doesn't affect you) 35 | * **BREAKING** `Chit.Settable` no longer implements `io.reactivex.disposables.Disposable`. 36 | * **BREAKING** Removed `RxGetter.fromVolatile`, `Breaker`, and `RateProbe`. 37 | * **BREAKING** Removed `RxavaCompat`. 38 | - **BREAKING** Removed OSGi metadata. 39 | 40 | ## [3.1.2] - 2021-10-21 41 | ### Fixed 42 | * Added some missing default methods in `Rx` 43 | 44 | ## [3.1.1] - 2021-10-21 45 | ### Fixed 46 | * Added some missing default methods in `RxSubscriber` 47 | 48 | ## [3.1.0] - 2021-10-21 49 | ### Added 50 | * Added support for kotlinx `Flow` and `Deferred`. ([#6](https://github.com/diffplug/durian-rx/pull/6)) 51 | 52 | ## [3.0.2] - 2020-05-26 53 | ### Fixed 54 | * `Chit.isDisposed()` now returns true before calling the `runWhenDisposed` callbacks. 55 | 56 | ## [3.0.1] - 2019-11-12 57 | * RxExecutor is now more consistent about failure - if the `onSuccess` throws an exception, it will always be passed to the `onFailure` handler as a `CompletionException`. 58 | 59 | ## [3.0.0] - 2018-08-01 60 | * `DisposableEar`'s final name is `Chit`. 61 | * Added `Rx.sync(RxBox a, RxBox b)`. 62 | * Added `MultiSelectModel` for a UI-independent multi-selection model. 63 | 64 | ## [3.0.0.BETA2] - 2017-03-08 65 | * Got rid of the `RxListener.IsLogging` marker interface. 66 | * Made `RxListener.isLogging()` public, and added `RxListener.onErrorDontLog(Throwable)`. 67 | + Combined, these methods make it possible for an external framework to detect and hijack logging for a specific listener. 68 | + Used by the Agent framework in DiffPlug 2+ 69 | * Added `CasBox.getAndSet()`. 70 | * `DispoableEar.Settable` now allows `dispose()` to be called multiple times, to comply with the `Disposable` contract. 71 | * An `RxJavaCompat` layer for turning `Single` and `Maybe` into `CompletionStage`. 72 | 73 | ## [3.0.0.BETA] - 2017-02-07 74 | * Added `DisposableEar` and `GuardedExecutor`. 75 | * Fixed a bug in `ForwardingBox.modify()`. 76 | * `RxExecutor` now exposes the underlying `Executor`, `Scheduler`, and `RxTracingPolicy`. 77 | 78 | ## [3.0.0.ALPHA] - 2016-11-11 79 | * Bumped RxJava to 2.0, and incorporated `RxTracingPolicy` into `RxJavaPlugins`. 80 | * Fixed a bug in `ForwardingBox.modify()`. 81 | * `RxExecutor` now exposes the underlying `Executor`, `Scheduler`, and `RxTracingPolicy`. 82 | 83 | ## [2.0.0] - 2016-07-13 84 | * `Immutables` has moved to `com.diffplug.durian:durian-collect`. 85 | * Removed collections-specific classes. 86 | + `RxOptional` -> `RxBox` 87 | + `RxSet` -> `RxBox` 88 | + This makes it possible to mix-and-match RxBox implementations and collection implementations. 89 | * `Box` and `RxBox` had poorly defined behavior around race conditions. It is now implemented by the following well-defined classes: 90 | + `RxBox.of(initialValue)` makes no atomicity guarantees. 91 | + `CasBox` supports compare-and-swap atomic modifications. 92 | + `LockBox` supports mutex-based atomic modifications. 93 | + `RxLockBox` supports mutex-based atomic modification with RxJava-based notifications. 94 | * Broke the overly crowded `Rx` class into serveral pieces: 95 | + `Rx` is now only a collection of utility methods. 96 | + `RxListener` is now the listener interface for `Observer & FutureCallback`. 97 | + `Rx.RxExecutor` is now `RxExecutor`, and `Rx.HasRxExecutor` is `RxExecutor.Has`. 98 | + `RxGetter` no longer enforces `distinctUntilChanged`. 99 | * Adopted Durian and its new `ConverterNonNull`. 100 | * Added `OrderedLock`, which takes multiple locks in a guaranteed lock-free way. 101 | * Added `Breaker`, for temporarily breaking a connection between observable values. 102 | 103 | ## [1.3.0] - 2016-02-09 104 | * Ditched Guava for DurianGuava. 105 | 106 | ## [1.2.0] - 2015-11-18 107 | * Added support for `CompletionStage` (and therefore `CompletableFuture`), with the same behavior as `ListenableFuture`. 108 | 109 | ## [1.1.0] - 2015-10-19 110 | * Changed OSGi metadata Bundle-SymbolicName to `com.diffplug.durian.rx`. 111 | * OSGi metadata is now auto-generated using bnd. 112 | 113 | ## [1.0.1] - 2015-07-27 114 | * Gah! MANIFEST.MF still had -SNAPSHOT version. Fixed now. Would be really nice if we could get MANIFEST.MF generation working. 115 | 116 | ## [1.0] - 2015-05-13 117 | * First stable release. 118 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Durian 2 | 3 | Pull requests are welcome, preferably against `master`. 4 | ## Build instructions 5 | 6 | It's a bog-standard gradle build. 7 | 8 | `gradlew eclipse` 9 | * creates an Eclipse project file for you. 10 | 11 | `gradlew build` 12 | * builds the jar 13 | * runs FindBugs 14 | * checks the formatting 15 | * runs the tests 16 | 17 | If you're getting style warnings, `gradlew spotlessApply` will apply anything necessary to fix formatting. For more info on the formatter, check out [spotless](https://github.com/diffplug/spotless). 18 | 19 | ## License 20 | 21 | By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/diffplug/durian/blob/master/LICENSE 22 | 23 | All files are released with the Apache 2.0 license as such: 24 | 25 | ``` 26 | Copyright 2020 DiffPlug 27 | 28 | Licensed under the Apache License, Version 2.0 (the "License"); 29 | you may not use this file except in compliance with the License. 30 | You may obtain a copy of the License at 31 | 32 | https://www.apache.org/licenses/LICENSE-2.0 33 | 34 | Unless required by applicable law or agreed to in writing, software 35 | distributed under the License is distributed on an "AS IS" BASIS, 36 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | See the License for the specific language governing permissions and 38 | limitations under the License. 39 | ``` 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DurianRx: Reactive getters, powered by RxJava and ListenableFuture 2 | 3 | 14 | [![Maven central](https://img.shields.io/badge/mavencentral-com.diffplug.durian%3Adurian--rx-blue.svg)](https://search.maven.org/artifact/com.diffplug.durian/durian-rx) 15 | [![Apache 2.0](https://img.shields.io/badge/license-apache--2.0-blue.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) 16 | 17 | [![Changelog](https://img.shields.io/badge/changelog-5.1.0-brightgreen.svg)](CHANGES.md) 18 | [![Javadoc](https://img.shields.io/badge/javadoc-yes-brightgreen.svg)](https://javadoc.io/doc/com.diffplug.durian/durian-rx/5.1.0/) 19 | [![Live chat](https://img.shields.io/badge/gitter-chat-brightgreen.svg)](https://gitter.im/diffplug/durian) 20 | [![JitCI](https://jitci.com/gh/diffplug/durian-rx/svg)](https://jitci.com/gh/diffplug/durian-rx) 21 | 22 | 23 | 26 | DurianRx unifies RxJava's [Observable](http://reactivex.io/documentation/observable.html) with Guava's [ListenableFuture](https://code.google.com/p/guava-libraries/wiki/ListenableFutureExplained). If you happen to be using SWT as a widget toolkit, then you'll want to look at [DurianSwt](https://github.com/diffplug/durian-swt) as well. 27 | 28 | ```java 29 | Observable observable = someObservable(); 30 | ListenableFuture future = someFuture(); 31 | Rx.subscribe(observable, val -> doSomething(val)); 32 | Rx.subscribe(future, val -> doSomething(val)); 33 | ``` 34 | 35 | It also provides [reactive getters](src/com/diffplug/common/rx/RxGetter.java?ts=4), a simple abstraction for piping data which allows access via `T get()` or `Observable asObservable()`. 36 | 37 | ```java 38 | RxBox mousePos = RxBox.of(new Point(0, 0)); 39 | this.addMouseListener(e -> mousePos.set(new Point(e.x, e.y))); 40 | 41 | Rectangle hotSpot = new Rectangle(0, 0, 10, 10) 42 | RxGetter isMouseOver = mousePos.map(hotSpot::contains); 43 | ``` 44 | 45 | Debugging an error which involves lots of callbacks can be difficult. To make this easier, DurianRx includes a [tracing capability](src/com/diffplug/common/rx/RxTracingPolicy.java?ts=4), which makes this task easier. 46 | 47 | ```java 48 | // anytime an error is thrown in an Rx callback, the stack trace of the error 49 | // will be wrapped by the stack trace of the original subscription 50 | DurianPlugins.register(RxTracingPolicy.class, new LogSubscriptionTrace()). 51 | ``` 52 | 53 | DurianRx's only requirements are [durian-base, durian-collect, durian-concurrent](https://github.com/diffplug/durian), and [RxJava](https://github.com/reactivex/rxjava). 54 | 55 | 56 | 57 | ## Acknowledgements 58 | 59 | * Many thanks to [RxJava](https://github.com/reactivex/rxjava) and [Guava](https://github.com/google/guava). 60 | * Built by [gradle](http://gradle.org/). 61 | * Tested by [junit](http://junit.org/). 62 | * Maintained by [DiffPlug](http://www.diffplug.com/). 63 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.diffplug.blowdryer' 3 | id 'com.diffplug.spotless-changelog' 4 | } 5 | 6 | spotlessChangelog { 7 | changelogFile 'CHANGES.md' 8 | } 9 | apply from: 干.file('base/changelog.gradle') 10 | apply plugin: 'java-library' 11 | apply from: 干.file('base/java.gradle') 12 | apply from: 干.file('base/kotlin.gradle') 13 | apply from: 干.file('spotless/freshmark.gradle') 14 | apply from: 干.file('spotless/java.gradle') 15 | spotlessChangelog { 16 | branch 'main' 17 | } 18 | 19 | 20 | def VER_DURIAN='1.2.0' 21 | def VER_DURIAN_DEBUG='1.0.0' 22 | def VER_JUNIT='4.12' 23 | 24 | dependencies { 25 | implementation "com.diffplug.durian:durian-core:${VER_DURIAN}" 26 | implementation "com.diffplug.durian:durian-concurrent:${VER_DURIAN}" 27 | api "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0" 28 | testImplementation "junit:junit:${VER_JUNIT}" 29 | testImplementation "com.diffplug.durian:durian-testlib:${VER_DURIAN}" 30 | testImplementation "com.diffplug.durian:durian-debug:${VER_DURIAN_DEBUG}" 31 | } 32 | 33 | ext.javadoc_links = [ 34 | "https://javadoc.io/doc/com.diffplug.durian/durian-core/${VER_DURIAN}", 35 | "https://javadoc.io/doc/com.diffplug.durian/durian-collect/${VER_DURIAN}", 36 | "https://javadoc.io/doc/com.diffplug.durian/durian-concurrent/${VER_DURIAN}", 37 | "https://javadoc.io/doc/com.diffplug.durian/durian-debug/${VER_DURIAN_DEBUG}", 38 | 'https://docs.oracle.com/javase/8/docs/api/' 39 | ].join(' ') 40 | apply from: 干.file('base/maven.gradle') 41 | apply from: 干.file('base/sonatype.gradle') 42 | 43 | //////////////////////// 44 | // SPOTBUGS (someday) // 45 | //////////////////////// 46 | dependencies { 47 | compileOnly 'com.google.code.findbugs:annotations:3.0.0' 48 | compileOnly 'com.google.code.findbugs:jsr305:3.0.0' 49 | } 50 | configurations { 51 | testImplementation.extendsFrom compileOnly 52 | } 53 | -------------------------------------------------------------------------------- /durian-rx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diffplug/durian-rx/d0a74a7772d41ad79b03e0363d9f3b00be373bdc/durian-rx.png -------------------------------------------------------------------------------- /durian.svg.license: -------------------------------------------------------------------------------- 1 | According to here: https://openclipart.org/detail/210244/misc-durian 2 | 3 | Glitch was a computer game whose visual assets were released into the public domain domain after the game failed commercially. The entire collection is about 2,000 clipart. 4 | 5 | http://en.wikipedia.org/wiki/Glitch_%28video_game%29 6 | 7 | The glitch assets were converted to svg format by Bart from opengameart.org 8 | 9 | Thanks to Glitch and Bart! 10 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | maven_name=durian-rx 2 | maven_group=com.diffplug.durian 3 | maven_desc=DurianRx: Reactive getters, powered by RxJava and ListenableFuture 4 | git_url=github.com/diffplug/durian-rx 5 | osgi_symbolic_name=com.diffplug.durian.rx 6 | osgi_export=com.diffplug.common.rx.* 7 | license=apache 8 | 9 | stable=3.0.1 10 | version=3.0.1 11 | name=durian-rx 12 | #group=com.diffplug.durian (moved to build.gradle to workaround gradle classpath problems) 13 | description=DurianRx - Reactive getters, powered by RxJava and ListenableFuture 14 | org=diffplug 15 | 16 | # Build requirements 17 | ver_java=17 18 | kotlin_jvmTarget=17 19 | -------------------------------------------------------------------------------- /gradle/spotless.eclipseformat.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | -------------------------------------------------------------------------------- /gradle/spotless.importorder: -------------------------------------------------------------------------------- 1 | #Organize Import Order 2 | #Fri Apr 24 02:36:28 PDT 2015 3 | 4=com.diffplug 4 | 3=com 5 | 2=org 6 | 1=javax 7 | 0=java 8 | -------------------------------------------------------------------------------- /gradle/spotless.license.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diffplug/durian-rx/d0a74a7772d41ad79b03e0363d9f3b00be373bdc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | plugins { 8 | // https://github.com/diffplug/blowdryer/blob/main/CHANGELOG.md 9 | id 'com.diffplug.blowdryerSetup' version '1.7.1' 10 | // https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md 11 | id 'com.diffplug.spotless' version '7.0.3' apply false 12 | // https://github.com/diffplug/spotless-changelog/blob/main/CHANGELOG.md 13 | id 'com.diffplug.spotless-changelog' version '3.1.2' apply false 14 | // https://plugins.gradle.org/plugin/com.gradle.plugin-publish 15 | id 'com.gradle.plugin-publish' version '1.3.1' apply false 16 | // https://github.com/equodev/equo-ide/blob/main/plugin-gradle/CHANGELOG.md 17 | id 'dev.equo.ide' version '1.7.8' apply false 18 | // https://github.com/gradle-nexus/publish-plugin/releases 19 | id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' apply false 20 | // https://plugins.gradle.org/plugin/org.jetbrains.dokka 21 | id 'org.jetbrains.dokka' version '2.0.0' apply false 22 | // https://plugins.gradle.org/plugin/org.jetbrains.kotlin.jvm 23 | id 'org.jetbrains.kotlin.jvm' version '2.1.21' apply false 24 | // https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.serialization 25 | id 'org.jetbrains.kotlin.plugin.serialization' version '2.1.21' apply false 26 | // https://plugins.gradle.org/plugin/org.jetbrains.kotlin.multiplatform 27 | id 'org.jetbrains.kotlin.multiplatform' version '2.1.21' apply false 28 | // https://github.com/adamko-dev/dokkatoo/releases 29 | id 'dev.adamko.dokkatoo-html' version '2.4.0' apply false 30 | } 31 | blowdryerSetup { 32 | github 'diffplug/blowdryer-diffplug', 'tag', '9.0.0' 33 | //devLocal '../blowdryer-diffplug' 34 | setPluginsBlockTo { 35 | it.file 'plugin.versions' 36 | it.file 'plugin-kotlin.versions' 37 | } 38 | } 39 | 40 | rootProject.name = 'durian-rx' 41 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/CasBox.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.base.Box; 20 | import com.diffplug.common.base.Converter; 21 | import java.util.function.Function; 22 | 23 | /** 24 | * CasBox is a lock-free and race-condition-free mechanism 25 | * for updating a value. 26 | * 27 | * Its API and implementation is taken straight from [Clojure's Atom](http://clojure.org/reference/atoms) 28 | * concept. Many thanks to Rich Hickey for his excellent work. 29 | */ 30 | public interface CasBox extends Box { 31 | /** The compare and set method which this box is capable of using. */ 32 | boolean compareAndSet(T expect, T update); 33 | 34 | /** Gets the current value, and sets it with a new one. */ 35 | T getAndSet(T newValue); 36 | 37 | /** 38 | * Applies the given mutator function to this box, which may require 39 | * calling the function more than once, so make sure it's pure! 40 | * 41 | * The function is called using the box's current input, and 42 | * {@link #compareAndSet(Object, Object) compareAndSet} is used to 43 | * ensure that the input does not change. 44 | * 45 | * The implementation is more or less verbatim from Rich Hickey's 46 | * [Clojure](https://github.com/clojure/clojure/blob/bfb82f86631bde45a8e3749ea7df509e59a0791c/src/jvm/clojure/lang/Atom.java#L75-L87). 47 | */ 48 | @Override 49 | default T modify(Function mutator) { 50 | while (true) { 51 | T value = get(); 52 | T newv = mutator.apply(value); 53 | if (compareAndSet(value, newv)) { 54 | return newv; 55 | } 56 | } 57 | } 58 | 59 | /** Returns a CasBox around the given value. */ 60 | public static CasBox of(T value) { 61 | return new CasBoxImp<>(value); 62 | } 63 | 64 | @Override 65 | default CasBox map(Converter converter) { 66 | return new CasBoxImp.Mapped<>(this, converter); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/CasBoxImp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.base.Converter; 20 | import java.util.concurrent.atomic.AtomicReference; 21 | 22 | class CasBoxImp implements CasBox { 23 | private final AtomicReference ref; 24 | 25 | CasBoxImp(T value) { 26 | ref = new AtomicReference<>(value); 27 | } 28 | 29 | @Override 30 | public boolean compareAndSet(T expect, T update) { 31 | return ref.compareAndSet(expect, update); 32 | } 33 | 34 | @Override 35 | public T getAndSet(T newValue) { 36 | return ref.getAndSet(newValue); 37 | } 38 | 39 | @Override 40 | public T get() { 41 | return ref.get(); 42 | } 43 | 44 | @Override 45 | public void set(T value) { 46 | ref.set(value); 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "CasBox.of[" + get() + "]"; 52 | } 53 | 54 | static class Mapped extends MappedImp> implements CasBox { 55 | public Mapped(CasBox delegate, Converter converter) { 56 | super(delegate, converter); 57 | } 58 | 59 | @Override 60 | public boolean compareAndSet(R expect, R update) { 61 | T expectOrig = converter.revertNonNull(expect); 62 | T updateOrig = converter.revertNonNull(update); 63 | return delegate.compareAndSet(expectOrig, updateOrig); 64 | } 65 | 66 | @Override 67 | public R getAndSet(R newValue) { 68 | T newValueOrig = converter.revertNonNull(newValue); 69 | T oldValueOrig = delegate.getAndSet(newValueOrig); 70 | return converter.convertNonNull(oldValueOrig); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/Chit.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2022 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | /** 23 | * Makes it possible to receive a notification when a resource is disposed. 24 | * 25 | * Oftentimes, a UI resource (such as a dialog) is subject to disposal. There 26 | * might be a long-running task in a non-UI thread which should be cancelled 27 | * if the dialog is closed. This interface provides a clean abstraction for 28 | * guaranteeing that an action is taken in response to a resource being disposed, 29 | * with exact semantics on the UI thread, and eventually-consistent semantics 30 | * on other threads. 31 | * 32 | * @see com.diffplug.common.swt.SwtRx#disposableEar(Widget) 33 | */ 34 | public interface Chit { 35 | /** 36 | * Returns whether the resource is disposed. May be called from any thread, 37 | * and must return true as soon as `dispose()` is called, no matter which thread 38 | * `dispose()` was called from. 39 | */ 40 | boolean isDisposed(); 41 | 42 | /** 43 | * Adds a listener which will run when the given element is disposed. 44 | * The runnable might be executed on any thread. If the element has 45 | * already been disposed (subject to the glitch constraints of 46 | * {@link #isDisposed}), the runnable will be executed immediately. 47 | */ 48 | void runWhenDisposed(Runnable whenDisposed); 49 | 50 | /** 51 | * Wraps the runnable such that it will only run iff the disposable has not been disposed, 52 | * according to {@link #isDisposed()}. 53 | */ 54 | default Runnable guard(Runnable delegate) { 55 | return new ChitImpl.GuardedRunnable(this, delegate); 56 | } 57 | 58 | /** Returns a {@link Chit} which has already been disposed. */ 59 | public static Chit alreadyDisposed() { 60 | return new ChitImpl.AlreadyDisposed(); 61 | } 62 | 63 | /** Creates a Settable Disposable ear. */ 64 | public static Settable settable() { 65 | return new Settable(); 66 | } 67 | 68 | /** The standard implementation of DisposableEar. */ 69 | public final class Settable implements Chit { 70 | ArrayList runWhenDisposed = new ArrayList<>(); 71 | 72 | private Settable() {} 73 | 74 | public void dispose() { 75 | List toDispose; 76 | synchronized (this) { 77 | toDispose = runWhenDisposed; 78 | runWhenDisposed = null; 79 | } 80 | if (toDispose != null) { 81 | toDispose.forEach(Runnable::run); 82 | } 83 | } 84 | 85 | public synchronized boolean isDisposed() { 86 | return runWhenDisposed == null; 87 | } 88 | 89 | public synchronized void runWhenDisposed(Runnable whenDisposed) { 90 | if (runWhenDisposed != null) { 91 | runWhenDisposed.add(whenDisposed); 92 | } else { 93 | whenDisposed.run(); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/ChitImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import java.util.Objects; 20 | 21 | class ChitImpl { 22 | private ChitImpl() {} 23 | 24 | static final class GuardedRunnable implements Runnable { 25 | final Chit guard; 26 | final Runnable delegate; 27 | 28 | public GuardedRunnable(Chit guard, Runnable delegate) { 29 | this.guard = Objects.requireNonNull(guard); 30 | this.delegate = Objects.requireNonNull(delegate); 31 | } 32 | 33 | @Override 34 | public void run() { 35 | if (!guard.isDisposed()) { 36 | delegate.run(); 37 | } 38 | } 39 | } 40 | 41 | static final class AlreadyDisposed implements Chit { 42 | @Override 43 | public boolean isDisposed() { 44 | return true; 45 | } 46 | 47 | @Override 48 | public void runWhenDisposed(Runnable whenDisposed) { 49 | whenDisposed.run(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/ForwardingBox.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.base.Box 19 | import kotlinx.coroutines.flow.Flow 20 | 21 | /** 22 | * Utility class for wrapping one kind of box with another. 23 | * - For wrapping a [CasBox], use [ForwardingBox.Cas]. 24 | * - For wrapping an [RxBox], use [ForwardingBox.Rx]. 25 | * - For wrapping a [LockBox], use [ForwardingBox.Lock]. 26 | * - For wrapping an [RxLockBox], use [ForwardingBox.RxLock]. 27 | * 28 | * Especially useful for overridding set(). 29 | */ 30 | open class ForwardingBox> 31 | protected constructor(protected val delegate: BoxType) : Box { 32 | override fun get(): T { 33 | return delegate.get() 34 | } 35 | 36 | override fun set(value: T) { 37 | delegate.set(value) 38 | } 39 | 40 | class Cas protected constructor(delegate: CasBox) : 41 | ForwardingBox>(delegate), CasBox { 42 | override fun compareAndSet(expect: T, update: T): Boolean { 43 | return delegate.compareAndSet(expect, update) 44 | } 45 | 46 | override fun getAndSet(newValue: T): T { 47 | return delegate.getAndSet(newValue) 48 | } 49 | } 50 | 51 | class Lock protected constructor(delegate: LockBox) : 52 | ForwardingBox>(delegate), LockBox { 53 | override fun lock(): Any { 54 | return delegate.lock() 55 | } 56 | } 57 | 58 | open class Rx protected constructor(delegate: RxBox) : 59 | ForwardingBox>(delegate), RxBox { 60 | override fun asFlow(): Flow { 61 | return delegate.asFlow() 62 | } 63 | } 64 | 65 | class RxLock protected constructor(delegate: RxLockBox) : 66 | ForwardingBox>(delegate), RxLockBox { 67 | override fun lock(): Any { 68 | return delegate.lock() 69 | } 70 | 71 | override fun asFlow(): Flow { 72 | return delegate.asFlow() 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/GuardedExecutor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2021 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.util.concurrent.ListenableFuture 19 | import java.util.* 20 | import java.util.concurrent.CompletionStage 21 | import java.util.concurrent.Executor 22 | import java.util.function.Supplier 23 | import kotlinx.coroutines.CoroutineScope 24 | import kotlinx.coroutines.Deferred 25 | import kotlinx.coroutines.Job 26 | import kotlinx.coroutines.SupervisorJob 27 | import kotlinx.coroutines.cancel 28 | import kotlinx.coroutines.flow.Flow 29 | import kotlinx.coroutines.launch 30 | 31 | /** 32 | * GuardedExecutor is an [Executor] and [RxSubscriber] which promises to cancel its subscriptions 33 | * and stop executing tasks once its [.getGuard] has been disposed. 34 | * 35 | * Useful for tying asynchronous tasks to gui elements. 36 | */ 37 | open class GuardedExecutor(val delegate: RxExecutor, val guard: Chit) : Executor, RxSubscriber { 38 | val scope: CoroutineScope by lazy { 39 | CoroutineScope(SupervisorJob() + delegate.dispatcher).apply { 40 | guard.runWhenDisposed { cancel() } 41 | } 42 | } 43 | 44 | fun launch(block: suspend CoroutineScope.() -> Unit): Job = scope.launch(block = block) 45 | 46 | override fun execute(command: Runnable) { 47 | delegate.executor.execute(guard.guard(command)) 48 | } 49 | 50 | /** Creates a runnable which runs on this Executor iff the guard widget is not disposed. */ 51 | fun wrap(delegate: Runnable): Runnable { 52 | return Runnable { execute(guard.guard(delegate)) } 53 | } 54 | 55 | private fun subscribe(subscriber: Supplier): Job { 56 | return if (!guard.isDisposed) { 57 | val job = subscriber.get() 58 | guard.runWhenDisposed { job.cancel() } 59 | job 60 | } else { 61 | Rx.sentinelJob 62 | } 63 | } 64 | 65 | override fun subscribeDisposable(flow: Flow, listener: RxListener): Job { 66 | return subscribe { delegate.subscribeDisposable(flow, listener) } 67 | } 68 | 69 | override fun subscribeDisposable(deferred: Deferred, listener: RxListener): Job { 70 | return subscribe { delegate.subscribeDisposable(deferred, listener) } 71 | } 72 | 73 | override fun subscribeDisposable( 74 | future: ListenableFuture, 75 | listener: RxListener 76 | ): Job { 77 | return subscribe { delegate.subscribeDisposable(future, listener) } 78 | } 79 | 80 | override fun subscribeDisposable( 81 | future: CompletionStage, 82 | listener: RxListener 83 | ): Job { 84 | return subscribe { delegate.subscribeDisposable(future, listener) } 85 | } 86 | 87 | override fun subscribe(flow: Flow, listener: RxListener) { 88 | subscribeDisposable(flow, listener) 89 | } 90 | 91 | override fun subscribe(deferred: Deferred, listener: RxListener) { 92 | subscribeDisposable(deferred, listener) 93 | } 94 | 95 | override fun subscribe(future: ListenableFuture, listener: RxListener) { 96 | subscribeDisposable(future, listener) 97 | } 98 | 99 | override fun subscribe(future: CompletionStage, listener: RxListener) { 100 | subscribeDisposable(future, listener) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/IFlowable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2025 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | import kotlinx.coroutines.flow.Flow; 19 | 20 | /** 21 | * An object which can be supplied in an {@link io.reactivex.Observable} form. 22 | * 23 | * Ideally, `io.reactivex.Observable` would be an interface, which would make this interface unnecessary. But 24 | * so long as it isn't, this (combined with {@link Rx}) makes it fairly seamless to fix this. 25 | */ 26 | public interface IFlowable { 27 | Flow asFlow(); 28 | 29 | /** Wraps an actual observable as an IObservable. */ 30 | static IFlowable wrap(Flow flow) { 31 | return () -> flow; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/LockBox.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.base.Box; 20 | import com.diffplug.common.base.Converter; 21 | import java.util.Arrays; 22 | import java.util.function.Function; 23 | import java.util.stream.Collectors; 24 | 25 | /** 26 | * LockBox is a box where every call to {@link #modify(Function)} 27 | * happens within a synchronized block. 28 | * 29 | * Using the {@link #transactOn(LockBox...)} method, you can obtain a lock 30 | * on multiple boxes in a way which is guaranteed to be free 31 | * of deadlock, so long as no one is getting a lock except through 32 | * {@link #modify(Function)} and {@link #transactOn(LockBox...)}. 33 | */ 34 | public interface LockBox extends Box { 35 | /** 36 | * The lock which is used by this LockBox's {@link #modify(Function) method}. 37 | * 38 | * For a LockBox which holds state, the LockBox itself is used. For a mapped 39 | * LockBox, the underlying LockBox which actually holds the state is used. 40 | */ 41 | Object lock(); 42 | 43 | @Override 44 | default T modify(Function mutator) { 45 | synchronized (lock()) { 46 | T result = mutator.apply(get()); 47 | set(result); 48 | return result; 49 | } 50 | } 51 | 52 | /** Creates a `LockBox` containing the given value, which uses itself as the lock. */ 53 | public static LockBox of(T value) { 54 | return new LockBoxImp<>(value); 55 | } 56 | 57 | /** Creates a `LockBox` containing the given value, and using the given object as the lock. */ 58 | public static LockBox of(T value, Object lock) { 59 | return new LockBoxImp<>(value, lock); 60 | } 61 | 62 | /** 63 | * Maps this LockBox to a new value which will have the 64 | * same lock as the original lock, since there's still 65 | * only one piece of state. 66 | */ 67 | @Override 68 | default LockBox map(Converter converter) { 69 | return new LockBoxImp.Mapped<>(this, converter); 70 | } 71 | 72 | /** 73 | * Creates an OrderedLock which allows running transactions on the given list of LockBoxes. 74 | * 75 | * This OrderedLock can be reused, and it is efficient to do so. 76 | */ 77 | public static OrderedLock transactOn(@SuppressWarnings("rawtypes") LockBox... locks) { 78 | return OrderedLock.on(Arrays.asList(locks).stream().map(LockBox::lock).collect(Collectors.toList())); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/LockBoxImp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.base.Converter; 20 | import java.util.Objects; 21 | 22 | class LockBoxImp implements LockBox { 23 | protected T value; 24 | protected final Object lock; 25 | 26 | protected LockBoxImp(T value) { 27 | this.value = value; 28 | this.lock = this; 29 | } 30 | 31 | protected LockBoxImp(T value, Object lock) { 32 | this.value = value; 33 | this.lock = lock; 34 | } 35 | 36 | @Override 37 | public Object lock() { 38 | return lock; 39 | } 40 | 41 | @Override 42 | public T get() { 43 | synchronized (lock()) { 44 | return value; 45 | } 46 | } 47 | 48 | @Override 49 | public void set(T value) { 50 | synchronized (lock()) { 51 | this.value = Objects.requireNonNull(value); 52 | } 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return "LockBox.of[" + get() + "]"; 58 | } 59 | 60 | static class Mapped extends MappedImp> implements LockBox { 61 | public Mapped(LockBox delegate, Converter converter) { 62 | super(delegate, converter); 63 | } 64 | 65 | /** 66 | * Ensures that we use the root delegate which 67 | * is actually holding the state as our lock. 68 | */ 69 | @Override 70 | public Object lock() { 71 | return delegate.lock(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/MappedImp.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.base.Box 19 | import com.diffplug.common.base.Converter 20 | import java.util.function.Function 21 | 22 | internal open class MappedImp>( 23 | @JvmField protected val delegate: BoxType, 24 | @JvmField protected val converter: Converter 25 | ) : Box { 26 | override fun get(): R = converter.convertNonNull(delegate.get()) 27 | 28 | override fun set(value: R) = delegate.set(converter.revertNonNull(value)) 29 | 30 | /** Shortcut for doing a set() on the result of a get(). */ 31 | override fun modify(mutator: Function): R { 32 | val result = Box.Nullable.ofNull() 33 | delegate.modify { input: T -> 34 | val unmappedResult = mutator.apply(converter.convertNonNull(input)) 35 | result.set(unmappedResult) 36 | converter.revertNonNull(unmappedResult) 37 | } 38 | return result.get() 39 | } 40 | 41 | override fun toString(): String = 42 | "[" + delegate + " mapped to " + get() + " by " + converter + "]" 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/MultiSelectModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.base.Box 19 | import com.diffplug.common.base.Converter 20 | import com.diffplug.common.base.Either 21 | import java.util.* 22 | import kotlinx.coroutines.flow.MutableSharedFlow 23 | 24 | /** Manages a selection based on a a MouseOver / Selection combination. */ 25 | class MultiSelectModel( 26 | val mouseOver: RxBox> = RxBox.of(Optional.empty()), 27 | val selection: RxBox> = RxBox.of(listOf()), 28 | val clicked: MutableSharedFlow = Rx.createEmitFlow() 29 | ) { 30 | var isCtrl = false 31 | 32 | /** Mouseover and selection in this model will trump whatever is in the other. */ 33 | fun trump(other: MultiSelectModel): Trumped { 34 | // make the selections impose exclusivity on themselves 35 | selectionExclusive(other) 36 | other.selectionExclusive(this) 37 | 38 | // maintain the combined selection 39 | val lastSelection = Box.ofVolatile(Either.createLeft, List>(listOf())) 40 | val getterSelection = 41 | RxGetter.combineLatest(selection, other.selection) { left: List, right: List -> 42 | if (!left.isEmpty() && !right.isEmpty()) { 43 | // if both are present, we'll keep what we've got, while the two work it out amongst 44 | // themselves 45 | lastSelection.get() 46 | } else { 47 | val newValue = 48 | if (left.isEmpty()) Either.createRight(right) 49 | else Either.createLeft, List>(left) 50 | lastSelection.set(newValue) 51 | newValue 52 | } 53 | } 54 | // when someone sets the combined selection, carry that over to the constituent selections 55 | val valueSelection = 56 | RxBox.from(getterSelection) { either: Either, List> -> 57 | either.acceptBoth(selection, other.selection, listOf(), listOf()) 58 | } 59 | 60 | // make the mouseOvers impose exclusivity on themselves 61 | mouseOverTrumps(other) 62 | 63 | // and maintain a combined mouseOver 64 | val lastMouseOver = 65 | Box.ofVolatile(Either.createLeft, Optional>(Optional.empty())) 66 | val getterMouseOver = 67 | RxGetter.combineLatest(mouseOver, other.mouseOver) { left: Optional, right: Optional 68 | -> 69 | if (left.isPresent && right.isPresent) { 70 | // if both are present, we'll keep what we've got, while the two work it out amongst 71 | // themselves 72 | lastMouseOver.get() 73 | } else { 74 | val newValue = 75 | if (left.isPresent) Either.createLeft(left) 76 | else Either.createRight, Optional>(right) 77 | lastMouseOver.set(newValue) 78 | newValue 79 | } 80 | } 81 | val valueMouseOver = 82 | RxBox.from(getterMouseOver) { either: Either, Optional> -> 83 | either.acceptBoth(mouseOver, other.mouseOver, Optional.empty(), Optional.empty()) 84 | } 85 | return Trumped(valueMouseOver, valueSelection) 86 | } 87 | 88 | /** A MultiSelectModel-ish which represents two trumped selections. */ 89 | class Trumped( 90 | val mouseOver: RxBox, Optional>>, 91 | val selection: RxBox, List>> 92 | ) { 93 | fun merge(isReallySecondary: (T) -> U?, wrap: (U) -> T): MultiSelectModel { 94 | fun toEither(t: T): Either = 95 | isReallySecondary(t)?.let { Either.createRight(it) } ?: Either.createLeft(t) 96 | val convOpt = 97 | Converter.from, Optional>, Optional>( 98 | { either -> either.fold({ t -> t }, { opt -> opt.map(wrap) }) }, 99 | { optT -> 100 | if (optT.isPresent) { 101 | val either = toEither(optT.get()) 102 | either.mapLeft { Optional.of(it) }.mapRight { Optional.of(it) } 103 | } else { 104 | Either.createLeft(Optional.empty()) 105 | } 106 | }) 107 | 108 | val convSet = 109 | Converter.from, List>, List>( 110 | { either -> either.fold({ t -> t }, { set -> set.map(wrap) }) }, 111 | { setT -> 112 | val builder = mutableListOf() 113 | setT.forEach { isReallySecondary(it)?.let { builder.add(it) } } 114 | if (builder.isEmpty()) { 115 | Either.createLeft(setT) 116 | } else { 117 | Either.createRight(builder) 118 | } 119 | }) 120 | return MultiSelectModel(mouseOver.map(convOpt), selection.map(convSet)) 121 | } 122 | } 123 | 124 | /** Separate from selectionExclusive to avoid infinite loop. */ 125 | private fun selectionExclusive(other: MultiSelectModel) { 126 | Rx.subscribe(selection) { newSelection: List -> 127 | // our selection has changed 128 | if (newSelection.isEmpty() || other.selection.get().isEmpty()) { 129 | return@subscribe 130 | } 131 | // and both us and our mutually-exclusive friend are non-empty 132 | // which means we've gotta empty somebody 133 | if (isCtrl) { 134 | // if we're in the middle of trying to set the selection using ctrl, 135 | // then we'll let our friend keep their selection and we'll sacrifice our own 136 | selection.set(listOf()) 137 | } else { 138 | // otherwise, we'll sabotage our friend 139 | other.selection.set(listOf()) 140 | } 141 | } 142 | } 143 | 144 | /** 145 | * Enforces that non-empty mouseOver on this MultiSelectManager will force mouseOver on the other 146 | * MultiSelectManager to be empty. 147 | */ 148 | private fun mouseOverTrumps(multiSelect: MultiSelectModel) { 149 | class EmptyEnforcer(private val single: RxBox>) { 150 | private var enabled = false 151 | 152 | init { 153 | Rx.subscribe(single) { next: Optional -> 154 | if (enabled && next.isPresent) { 155 | single.set(Optional.empty()) 156 | } 157 | } 158 | } 159 | 160 | fun setEnabled(enabled: Boolean) { 161 | this.enabled = enabled 162 | if (enabled) { 163 | single.set(Optional.empty()) 164 | } 165 | } 166 | } 167 | 168 | val emptyEnforcer = EmptyEnforcer(multiSelect.mouseOver) 169 | Rx.subscribe(mouseOver) { next: Optional -> emptyEnforcer.setEnabled(next.isPresent) } 170 | } 171 | 172 | companion object { 173 | /** Creates an Optional from an Either. */ 174 | fun optEitherFrom( 175 | either: Either, Optional> 176 | ): Optional> { 177 | return either.fold({ leftOpt: Optional -> 178 | leftOpt.map { l: T -> Either.createLeft(l) } 179 | }) { rightOpt: Optional -> 180 | rightOpt.map { r: U -> Either.createRight(r) } 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/OrderedLock.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2022 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | import static java.util.Objects.requireNonNull; 19 | 20 | import java.util.Arrays; 21 | import java.util.BitSet; 22 | import java.util.Collection; 23 | import java.util.Comparator; 24 | import java.util.function.Supplier; 25 | 26 | /** 27 | * All code which takes locks using this code is guaranteed to do so 28 | * in the same order, guaranteeing there won't be a deadlock. 29 | * 30 | * `OrderedLock` instances can be reused as many times as 31 | * you'd like, and they're expensive to create and cheap to keep 32 | * around, so reuse is highly recommended. 33 | * 34 | * Thanks to Brian Goetz and Tim Peierls for their great demo for how to handle 35 | * the case that they have the same hashcode: 36 | * https://books.google.com/books?id=mzgFCAAAQBAJ&pg=PA208&dq=System.identityHashCode&hl=en&sa=X&ved=0ahUKEwjUvrLcjufMAhWG4IMKHS4NDVwQ6AEIKzAC#v=onepage&q=System.identityHashCode&f=false 37 | */ 38 | public class OrderedLock { 39 | /** Monitor object for breaking ties for objects with the same hashCode. */ 40 | private static final Object tieBreaker = new Object(); 41 | 42 | /** Creates an OrderedLock for the given collection of locks. */ 43 | public static OrderedLock on(Collection locks) { 44 | return on(locks.toArray()); 45 | } 46 | 47 | /** Creates an OrderedLock for the given array of locks. */ 48 | public static OrderedLock on(Object... locks) { 49 | // find any duplicates and create an array of Object 50 | // that contains only the unique locks. 51 | // 52 | // duplicate locks don't affect correctness, but they 53 | // will require taking the tieBreaker object, which 54 | // would then effectively become a single global lock 55 | BitSet isDuplicate = new BitSet(locks.length); 56 | for (int i = 0; i < locks.length; ++i) { 57 | Object underTest = locks[i]; 58 | for (int j = i + 1; j < locks.length; ++j) { 59 | if (underTest == locks[j]) { 60 | isDuplicate.set(i); 61 | break; 62 | } 63 | } 64 | } 65 | int numDuplicates = isDuplicate.cardinality(); 66 | if (numDuplicates == 0) { 67 | return new OrderedLock(locks); 68 | } else { 69 | Object[] noDuplicates = new Object[locks.length - numDuplicates]; 70 | int idx = 0; 71 | int i = 0; 72 | while (idx < noDuplicates.length) { 73 | if (!isDuplicate.get(i)) { 74 | noDuplicates[idx] = locks[i]; 75 | ++idx; 76 | } 77 | ++i; 78 | } 79 | for (Object lock : noDuplicates) { 80 | requireNonNull(lock); 81 | } 82 | return new OrderedLock(noDuplicates); 83 | } 84 | } 85 | 86 | private final Object[] locks; 87 | private final boolean needsTieBreaker; 88 | 89 | private OrderedLock(Object[] locks) { 90 | Arrays.sort(locks, Comparator.comparing(System::identityHashCode)); 91 | boolean needsTieBreaker = false; 92 | // if any of the locks have the same identity hashCode, then we're going to need a tiebreaker 93 | for (int i = 0; i < locks.length - 1; ++i) { 94 | if (System.identityHashCode(locks[i]) == System.identityHashCode(locks[i + 1])) { 95 | needsTieBreaker = true; 96 | break; 97 | } 98 | } 99 | this.locks = locks; 100 | this.needsTieBreaker = needsTieBreaker; 101 | } 102 | 103 | /** Takes the locks in a globally consistent order, then calls the given supplier and returns its value. */ 104 | public T takeAndGet(Supplier toGet) { 105 | if (needsTieBreaker) { 106 | synchronized (tieBreaker) { 107 | return takeAndGet(toGet, 0); 108 | } 109 | } else { 110 | return takeAndGet(toGet, 0); 111 | } 112 | } 113 | 114 | private T takeAndGet(Supplier toGet, int i) { 115 | if (i == locks.length) { 116 | return toGet.get(); 117 | } else { 118 | synchronized (locks[i]) { 119 | return takeAndGet(toGet, i + 1); 120 | } 121 | } 122 | } 123 | 124 | /** Takes the locks in a globally consistent order, then runs the given Runnable. */ 125 | public void takeAndRun(Runnable toRun) { 126 | takeAndGet(() -> { 127 | toRun.run(); 128 | return null; 129 | }); 130 | } 131 | 132 | /** Returns a Runnable which will get and release the appropriate locks before executing its argument. */ 133 | public Runnable wrap(Runnable toWrap) { 134 | requireNonNull(toWrap); 135 | return () -> takeAndRun(toWrap); 136 | } 137 | 138 | /** Returns a Supplier which will get and release the appropriate locks before executing its argument. */ 139 | public Supplier wrap(Supplier toWrap) { 140 | requireNonNull(toWrap); 141 | return () -> takeAndGet(toWrap); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/Rx.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2022 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.base.Box 19 | import com.diffplug.common.base.Consumers 20 | import com.diffplug.common.base.DurianPlugins 21 | import com.diffplug.common.base.Either 22 | import com.diffplug.common.base.Errors 23 | import com.diffplug.common.rx.RxListener.DefaultTerminate 24 | import com.diffplug.common.util.concurrent.ListenableFuture 25 | import com.diffplug.common.util.concurrent.MoreExecutors 26 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings 27 | import java.lang.IllegalStateException 28 | import java.lang.SafeVarargs 29 | import java.util.* 30 | import java.util.concurrent.CompletionStage 31 | import java.util.concurrent.Executor 32 | import java.util.concurrent.Future 33 | import java.util.function.Consumer 34 | import kotlinx.coroutines.CoroutineDispatcher 35 | import kotlinx.coroutines.Deferred 36 | import kotlinx.coroutines.Job 37 | import kotlinx.coroutines.asCoroutineDispatcher 38 | import kotlinx.coroutines.channels.BufferOverflow 39 | import kotlinx.coroutines.channels.Channel 40 | import kotlinx.coroutines.flow.Flow 41 | import kotlinx.coroutines.flow.MutableSharedFlow 42 | import kotlinx.coroutines.flow.merge 43 | 44 | /** 45 | * Unifies the listener models of [RxJava's Observable][io.reactivex.Observable] with ` 46 | * [Guava's ListenableFuture](https://code.google.com/p/guava-libraries/wiki/ListenableFutureExplained) 47 | * ` , and also adds tracing capabilities. 48 | * 49 | * TL;DR
 // subscribe to values, termination, or both Rx.subscribe(listenableOrObservable, val
 50 |  * -> doSomething(val)); // errors are passed to Errors.log() Rx.subscribe(listenableOrObservable,
 51 |  * Rx.onTerminate(optionalError -> maybeHandleError()); // values are ignored
 52 |  * Rx.subscribe(listenableOrObservable, Rx.onValueOrTerminate(val -> doSomething(val), optionalError
 53 |  * -> maybeHandleError())); // receive callbacks on a specific executor
 54 |  * Rx.on(someExecutor).subscribe(listenableOrObservable, val -> doSomething(val)); // call
 55 |  * unsubscribe() on the subscription to cancel it io.reactivex.disposables.Disposable subscription =
 56 |  * Rx.subscribe(listenableOrObservable, val -> doSomething); 
* Long version: `Rx` implements 57 | * both the [io.reactivex.Observer] and [com.diffplug.common.util.concurrent.FutureCallback] 58 | * interfaces by mapping them to two `Consumer`s: 59 | * * `Consumer onValue` 60 | * * `Consumer> onTerminate` 61 | * 62 | * Which are mapped as follows: 63 | * * `Observable.onNext(T value) -> onValue.accept(value)` 64 | * * `Observable.onCompleted() -> onTerminate.accept(Optional.empty())` 65 | * * `Observable.onError(Throwable error) -> onTerminate.accept(Optional.of(error))` 66 | * * `FutureCallback.onSuccess(T value) -> onValue.accept(value); 67 | * onTerminate.accept(Optional.empty());` 68 | * * `FutureCallback.onError(Throwable error) -> onTerminate.accept(Optional.of(error))` 69 | * 70 | * An instance of Rx is created by calling one of Rx's static creator methods: 71 | * * [onValue(Consumer&lt;T&gt;)][.onValue] 72 | * * [onTerminate(Consumer&lt;Optional&lt;Throwable&gt;&gt;)][.onTerminate] 73 | * * [onFailure(Consumer&lt;Throwable&gt;)][.onFailure] 74 | * * [onValueOrTerminate(Consumer&lt;T&gt;, 75 | * Consumer&lt;Optional&lt;Throwable&gt;&gt;)][.onValueOnTerminate] 76 | * * [onValueOrFailure(Consumer&lt;T&gt;, 77 | * Consumer&lt;Throwable&gt;)][.onValueOnFailure] 78 | * 79 | * Once you have an instance of Rx, you can subscribe it using the normal RxJava or Guava calls: 80 | * * `rxObservable.subscribe(Rx.onValue(val -> doSomething(val));` 81 | * * `Futures.addCallback(listenableFuture, Rx.onValue(val -> doSomething(val));` 82 | * 83 | * But the recommended way to subscribe is to use: 84 | * * `Rx.subscribe(listenableOrObservable, Rx.onValue(val -> doSomething(val)));` 85 | * * `Rx.subscribe(listenableOrObservable, val -> doSomething(val)); // automatically uses 86 | * Rx.onValue()` 87 | * 88 | * The advantage of this latter method is that it returns [io.reactivex.disposables.Disposable] 89 | * instances which allow you to unsubscribe from futures in the same manner as for observables. 90 | * * `subscription = Rx.subscribe( ... )` 91 | * 92 | * If you wish to receive callbacks on a specific thread, you can use: 93 | * * `Rx.on(someExecutor).subscribe( ... )` 94 | * 95 | * Because RxJava's Observables use [io.reactivex.Scheduler]s rather than 96 | * [java.util.concurrent.Executor]s, a Scheduler is automatically created using [Schedulers.from]. 97 | * If you'd like to specify the Scheduler manually, you can use [Rx.callbackOn] or you can create an 98 | * executor which implements [RxExecutor.Has]. 99 | * 100 | * @see [SwtExec] 101 | * (https://diffplug.github.io/durian-swt/javadoc/snapshot/com/diffplug/common/swt/SwtExec.html) 102 | */ 103 | object Rx { 104 | @JvmStatic 105 | fun createEmitFlow() = 106 | MutableSharedFlow( 107 | replay = 0, extraBufferCapacity = Channel.UNLIMITED, BufferOverflow.SUSPEND) 108 | 109 | @JvmStatic 110 | fun emit(flow: MutableSharedFlow, value: T) { 111 | if (!flow.tryEmit(value)) { 112 | throw IllegalStateException("Failed to emit $value on $flow") 113 | } 114 | } 115 | 116 | /** 117 | * Creates an Rx instance which will call the given consumer whenever a value is received. Any 118 | * errors are sent to ErrorHandler.log(). 119 | */ 120 | @JvmStatic 121 | fun onValue(onValue: Consumer): RxListener { 122 | return RxListener(onValue, RxListener.logErrors) 123 | } 124 | 125 | /** 126 | * Creates an Rx instance which will call the given consumer whenever the followed stream or 127 | * future completes, whether with an error or not. 128 | */ 129 | @JvmStatic 130 | fun onTerminate(onTerminate: Consumer>): RxListener { 131 | return RxListener(Consumers.doNothing(), onTerminate) 132 | } 133 | 134 | /** 135 | * Creates an Rx instance which will call the given consumer whenever the followed stream or 136 | * future completes, whether with an error or not, and the error (if present) will be logged. 137 | */ 138 | @JvmStatic 139 | fun onTerminateLogError(onTerminate: Consumer>): RxListener { 140 | return RxListener(Consumers.doNothing(), DefaultTerminate(onTerminate)) 141 | } 142 | 143 | /** 144 | * Creates an Rx instance which will call the given consumer whenever the followed stream or 145 | * future completes with an error. 146 | */ 147 | @JvmStatic 148 | fun onFailure(onFailure: Consumer): RxListener { 149 | Objects.requireNonNull(onFailure) 150 | return RxListener(Consumers.doNothing()) { error: Optional -> 151 | if (error.isPresent) { 152 | onFailure.accept(error.get()) 153 | } 154 | } 155 | } 156 | 157 | /** 158 | * Creates an Rx instance which will call onValue whenever a value is received, is received, and 159 | * onTerminate when the future or observable completes, whether with an error or not. 160 | */ 161 | @JvmStatic 162 | fun onValueOnTerminate( 163 | onValue: Consumer, 164 | onTerminate: Consumer> 165 | ): RxListener { 166 | return RxListener(onValue, onTerminate) 167 | } 168 | 169 | /** 170 | * Creates an Rx instance which will call the given consumer whenever the followed stream or 171 | * future completes, whether with an error or not, and the error (if present) will automatically 172 | * be logged. 173 | */ 174 | @JvmStatic 175 | fun onValueOnTerminateLogError( 176 | onValue: Consumer, 177 | onTerminate: Consumer> 178 | ): RxListener { 179 | return RxListener(onValue, DefaultTerminate(onTerminate)) 180 | } 181 | 182 | /** 183 | * Creates an Rx instance which will call onValue whenever a value is received, and onFailure if 184 | * the stream or future completes with an error. 185 | */ 186 | @JvmStatic 187 | fun onValueOnFailure(onValue: Consumer, onFailure: Consumer): RxListener { 188 | Objects.requireNonNull(onFailure) 189 | return RxListener(onValue) { error: Optional -> 190 | if (error.isPresent) { 191 | onFailure.accept(error.get()) 192 | } 193 | } 194 | } 195 | 196 | // Static versions 197 | @JvmStatic 198 | fun subscribe(flow: Flow, listener: RxListener) { 199 | sameThreadExecutor().subscribe(flow, listener) 200 | } 201 | 202 | @JvmStatic 203 | fun subscribe(flow: Flow, listener: Consumer) { 204 | subscribe(flow, onValue(listener)) 205 | } 206 | 207 | @JvmStatic 208 | fun subscribe(deferred: Deferred, listener: RxListener) { 209 | sameThreadExecutor().subscribe(deferred, listener) 210 | } 211 | 212 | @JvmStatic 213 | fun subscribe(deferred: Deferred, listener: Consumer) { 214 | subscribe(deferred, onValue(listener)) 215 | } 216 | 217 | @JvmStatic 218 | fun subscribe(flow: IFlowable, listener: RxListener) { 219 | subscribe(flow.asFlow(), listener) 220 | } 221 | 222 | @JvmStatic 223 | fun subscribe(flow: IFlowable, listener: Consumer) { 224 | subscribe(flow.asFlow(), listener) 225 | } 226 | 227 | @JvmStatic 228 | fun subscribe(future: ListenableFuture, listener: RxListener) { 229 | sameThreadExecutor().subscribe(future, listener) 230 | } 231 | 232 | @JvmStatic 233 | fun subscribe(future: ListenableFuture, listener: Consumer) { 234 | subscribe(future, onValueOnTerminate(listener, TrackCancelled(future))) 235 | } 236 | 237 | @JvmStatic 238 | fun subscribe(future: CompletionStage, listener: RxListener) { 239 | sameThreadExecutor().subscribe(future, listener) 240 | } 241 | 242 | @JvmStatic 243 | fun subscribe(future: CompletionStage, listener: Consumer) { 244 | subscribe(future, onValueOnTerminate(listener, TrackCancelled(future.toCompletableFuture()))) 245 | } 246 | 247 | // Static versions 248 | @JvmStatic 249 | fun subscribeDisposable(flow: Flow, listener: RxListener): Job { 250 | return sameThreadExecutor().subscribeDisposable(flow, listener) 251 | } 252 | 253 | @JvmStatic 254 | fun subscribeDisposable(flow: Flow, listener: Consumer): Job { 255 | return subscribeDisposable(flow, onValue(listener)) 256 | } 257 | 258 | @JvmStatic 259 | fun subscribeDisposable(deferred: Deferred, listener: RxListener): Job { 260 | return sameThreadExecutor().subscribeDisposable(deferred, listener) 261 | } 262 | 263 | @JvmStatic 264 | fun subscribeDisposable(deferred: Deferred, listener: Consumer): Job { 265 | return subscribeDisposable(deferred, onValue(listener)) 266 | } 267 | 268 | @JvmStatic 269 | fun subscribeDisposable(flow: IFlowable, listener: RxListener): Job { 270 | return subscribeDisposable(flow.asFlow(), listener) 271 | } 272 | 273 | @JvmStatic 274 | fun subscribeDisposable(flow: IFlowable, listener: Consumer): Job { 275 | return subscribeDisposable(flow.asFlow(), listener) 276 | } 277 | 278 | @JvmStatic 279 | fun subscribeDisposable(future: ListenableFuture, listener: RxListener): Job { 280 | return sameThreadExecutor().subscribeDisposable(future, listener) 281 | } 282 | 283 | @JvmStatic 284 | fun subscribeDisposable(future: ListenableFuture, listener: Consumer): Job { 285 | return subscribeDisposable(future, onValueOnTerminate(listener, TrackCancelled(future))) 286 | } 287 | 288 | @JvmStatic 289 | fun subscribeDisposable(future: CompletionStage, listener: RxListener): Job { 290 | return sameThreadExecutor().subscribeDisposable(future, listener) 291 | } 292 | 293 | @JvmStatic 294 | fun subscribeDisposable(future: CompletionStage, listener: Consumer): Job { 295 | return subscribeDisposable( 296 | future, onValueOnTerminate(listener, TrackCancelled(future.toCompletableFuture()))) 297 | } 298 | 299 | /** 300 | * Mechanism for specifying a specific Executor. A corresponding Scheduler will be created using 301 | * Schedulers.from(executor). 302 | */ 303 | @JvmStatic 304 | fun callbackOn(executor: Executor): RxExecutor { 305 | return if (executor === MoreExecutors.directExecutor()) { 306 | sameThreadExecutor() 307 | } else if (executor is RxExecutor.Has) { 308 | executor.rxExecutor 309 | } else { 310 | RxExecutor(executor, executor.asCoroutineDispatcher()) 311 | } 312 | } 313 | 314 | @JvmStatic 315 | fun callbackOn(executor: Executor, dispatcher: CoroutineDispatcher): RxExecutor { 316 | return RxExecutor(executor, dispatcher) 317 | } 318 | 319 | @JvmStatic 320 | @SuppressFBWarnings( 321 | value = ["LI_LAZY_INIT_STATIC"], 322 | justification = "This race condition is fine, as explained in the comment below.") 323 | fun sameThreadExecutor(): RxExecutor { 324 | // There is an acceptable race condition here - _sameThread might get set multiple times. 325 | // This would happen if multiple threads called blocking() at the same time 326 | // during initialization, and this is likely to actually happen in practice. 327 | // 328 | // It is important for this method to be fast, so it's better to accept 329 | // that getSameThreadExecutor() might return different instances (which each have the 330 | // same behavior), rather than to incur the cost of some type of synchronization. 331 | if (_sameThread == null) { 332 | _sameThread = 333 | RxExecutor( 334 | MoreExecutors.directExecutor(), 335 | MoreExecutors.directExecutor().asCoroutineDispatcher()) 336 | } 337 | return _sameThread!! 338 | } 339 | 340 | private var _sameThread: RxExecutor? = 341 | null // if it isn't an RxListener, then we'll apply _tracing policy// if it's an RxListener, 342 | // then _tracingPolicy handled it already// There is an acceptable race condition here - 343 | // see getSameThreadExecutor() 344 | 345 | /** Returns the global tracing policy. */ 346 | @get:SuppressFBWarnings( 347 | value = ["LI_LAZY_INIT_STATIC", "LI_LAZY_INIT_UPDATE_STATIC"], 348 | justification = "This race condition is fine, as explained in the comment below.") 349 | val tracingPolicy: RxTracingPolicy 350 | get() { 351 | // There is an acceptable race condition here - see getSameThreadExecutor() 352 | if (_tracingPolicy == null) { 353 | _tracingPolicy = DurianPlugins.get(RxTracingPolicy::class.java, RxTracingPolicy.NONE) 354 | if (_tracingPolicy !== RxTracingPolicy.NONE) { 355 | // TODO: setup tracing 356 | } 357 | } 358 | return _tracingPolicy!! 359 | } 360 | 361 | private var _tracingPolicy: RxTracingPolicy? = null 362 | 363 | /** Package-private for testing - resets all of the static member variables. */ 364 | fun resetForTesting() { 365 | _sameThread = null 366 | _tracingPolicy = null 367 | } 368 | 369 | /** Merges a bunch of [IFlowable]s into a single [Flow] containing the most-recent value. */ 370 | @JvmStatic 371 | @SafeVarargs 372 | fun merge(vararg toMerge: IFlowable): Flow { 373 | return toMerge.map { it.asFlow() }.merge() 374 | } 375 | 376 | /** Reliable way to sync two RxBox to each other. */ 377 | @JvmStatic 378 | fun sync(left: RxBox, right: RxBox) { 379 | sync(sameThreadExecutor(), left, right) 380 | } 381 | 382 | /** 383 | * Reliable way to sync two RxBox to each other, using the given RxSubscriber to listen for 384 | * changes 385 | */ 386 | @JvmStatic 387 | fun sync(subscriber: RxSubscriber, left: RxBox, right: RxBox) { 388 | val firstChange = Box.Nullable.ofNull?>() 389 | subscriber.subscribe(left) { leftVal: T -> 390 | // the left changed before we could acknowledge it 391 | if (leftVal != left.get()) { 392 | return@subscribe 393 | } 394 | var doSet: Boolean 395 | synchronized(firstChange) { 396 | val change = firstChange.get() 397 | doSet = change == null || change.isLeft 398 | if (!doSet) { 399 | // this means the right set something before we did 400 | if (leftVal == change!!.right) { 401 | // if we're setting to the value that the right 402 | // requested, then we're just responding to them, 403 | // and there's no need to fire another event 404 | firstChange.set(null) 405 | } else { 406 | // otherwise, we'll record that we set it first 407 | firstChange.set(Either.createLeft(leftVal)) 408 | } 409 | } 410 | } 411 | if (doSet) { 412 | right.set(leftVal) 413 | } 414 | } 415 | subscriber.subscribe(right) { rightVal: T -> 416 | // the right changed before we could acknowledge it 417 | if (rightVal != right.get()) { 418 | return@subscribe 419 | } 420 | var doSet: Boolean 421 | synchronized(firstChange) { 422 | val change = firstChange.get() 423 | doSet = change == null || change.isRight 424 | if (!doSet) { 425 | // this means the left set something before we did 426 | if (rightVal == change!!.left) { 427 | // if we're setting to the value that the left 428 | // requested, then we're just responding to them, 429 | // and there's no need to fire another event 430 | firstChange.set(null) 431 | } else { 432 | // otherwise, we'll record that we set it first 433 | firstChange.set(Either.createRight(rightVal)) 434 | } 435 | } 436 | } 437 | if (doSet) { 438 | left.set(rightVal) 439 | } 440 | } 441 | } 442 | 443 | /** 444 | * An error listener which tracks whether a future has been cancelled, so that it doesn't log the 445 | * errors of cancelled futures. 446 | */ 447 | internal class TrackCancelled(private val future: Future<*>) : 448 | DefaultTerminate(Consumers.doNothing()) { 449 | override fun accept(errorOpt: Optional) { 450 | if (errorOpt.isPresent && !future.isCancelled) { 451 | Errors.log().accept(errorOpt.get()) 452 | } 453 | } 454 | } 455 | 456 | @JvmStatic val sentinelJob: Job = Job().apply { cancel() } 457 | } 458 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/RxBox.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.base.Box 19 | import com.diffplug.common.base.Converter 20 | import com.diffplug.common.rx.Rx.subscribe 21 | import java.util.function.Consumer 22 | import java.util.function.Function 23 | import kotlinx.coroutines.flow.Flow 24 | import kotlinx.coroutines.flow.map 25 | 26 | /** [RxGetter] and [Box] combined in one: a value you can set, get, and subscribe to. */ 27 | interface RxBox : RxGetter, Box { 28 | /** Returns a read-only version of this `RxBox`. */ 29 | fun readOnly(): RxGetter = this 30 | 31 | /** Maps one `RxBox` to another `RxBox`. */ 32 | override fun map(converter: Converter): RxBox { 33 | return RxBoxImp.Mapped(this, converter) 34 | } 35 | 36 | /** 37 | * Provides a mechanism for enforcing an invariant on an existing `RxBox`. 38 | * 39 | * The returned `RxBox` and its observable will **always** satisfy the given invariant. If the 40 | * underlying `RxBox` changes in a way which does not satisfy the invariant, it will be set so 41 | * that it does match the invariant. 42 | * 43 | * During this process, the underlying `RxBox` will momentarily fail to meet the invariant, and 44 | * its `Observable` will emit values which fail the invariant. The returned `RxBox`, however, will 45 | * always meet the invariant, so downstream consumers can rely on the invariant holding true at 46 | * all times. 47 | * 48 | * The returned `RxBox` can be mapped, and has the same atomicity guarantees as the underlying 49 | * `RxBox` (e.g. an enforced [RxLockBox] can still be modified atomically). 50 | * 51 | * Conflicting calls to `enforce` can cause an infinite loop, see [Breaker] for a possible 52 | * solution. 53 | * 54 | * ```java 55 | * // this will not end well... 56 | * RxBox.of(1).enforce(Math::abs).enforce(i -> -Math.abs(i)); 57 | * ``` 58 | */ 59 | fun enforce(enforcer: Function): RxBox { 60 | // this must be a plain-old observable, because it needs to fire 61 | // every time an invariant is violated, not only when a violation 62 | // of the invariant causes a change in the output 63 | val mapped = asFlow().map { t: T -> enforcer.apply(t) } 64 | subscribe(mapped) { value: T -> this.set(value) } 65 | // now we can return the RxBox 66 | return map(Converter.from(enforcer, enforcer)) 67 | } 68 | 69 | companion object { 70 | /** Creates an `RxBox` with the given initial value. */ 71 | @JvmStatic fun of(initial: T): RxBox = RxBoxImp(initial) 72 | 73 | /** 74 | * Creates an `RxBox` which implements the "getter" part with `RxGetter`, and the setter part 75 | * with `Consumer`. 76 | */ 77 | @JvmStatic 78 | fun from(getter: RxGetter, setter: Consumer): RxBox = 79 | object : RxBox { 80 | override fun asFlow(): Flow { 81 | return getter.asFlow() 82 | } 83 | 84 | override fun get(): T { 85 | return getter.get() 86 | } 87 | 88 | override fun set(value: T) { 89 | setter.accept(value) 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/RxBoxImp.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.base.Converter 19 | import kotlinx.coroutines.flow.Flow 20 | import kotlinx.coroutines.flow.MutableStateFlow 21 | import kotlinx.coroutines.flow.distinctUntilChanged 22 | import kotlinx.coroutines.flow.map 23 | 24 | internal open class RxBoxImp(initial: T) : RxBox { 25 | private val subject = MutableStateFlow(initial) 26 | 27 | override fun set(newValue: T) { 28 | if (subject.value != newValue) { 29 | subject.value = newValue 30 | } 31 | } 32 | 33 | override fun get(): T = subject.value 34 | 35 | override fun asFlow(): Flow = subject 36 | 37 | internal class Mapped(delegate: RxBox, converter: Converter) : 38 | MappedImp>(delegate, converter), RxBox { 39 | val flow: Flow = delegate.asFlow().map(converter::convertNonNull).distinctUntilChanged() 40 | 41 | override fun asFlow(): Flow = flow 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/RxExecutor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2022 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.base.Errors 19 | import com.diffplug.common.util.concurrent.ListenableFuture 20 | import java.lang.Error 21 | import java.util.* 22 | import java.util.concurrent.CompletionException 23 | import java.util.concurrent.CompletionStage 24 | import java.util.concurrent.Executor 25 | import kotlinx.coroutines.CancellationException 26 | import kotlinx.coroutines.CoroutineDispatcher 27 | import kotlinx.coroutines.CoroutineScope 28 | import kotlinx.coroutines.Deferred 29 | import kotlinx.coroutines.Job 30 | import kotlinx.coroutines.flow.Flow 31 | import kotlinx.coroutines.flow.launchIn 32 | import kotlinx.coroutines.flow.onCompletion 33 | import kotlinx.coroutines.flow.onEach 34 | import kotlinx.coroutines.launch 35 | 36 | class RxExecutor internal constructor(val executor: Executor, val dispatcher: CoroutineDispatcher) : 37 | RxSubscriber { 38 | 39 | fun launch(block: suspend CoroutineScope.() -> Unit): Job = 40 | CoroutineScope(Job() + dispatcher).launch(block = block) 41 | 42 | interface Has : Executor { 43 | val rxExecutor: RxExecutor 44 | } 45 | 46 | override fun subscribe(flow: Flow, listener: RxListener) { 47 | subscribeDisposable(flow, listener) 48 | } 49 | 50 | override fun subscribe(deferred: Deferred, listener: RxListener) { 51 | subscribeDisposable(deferred, listener) 52 | } 53 | 54 | override fun subscribe(future: ListenableFuture, untracedListener: RxListener) { 55 | val listener = Rx.tracingPolicy.hook(future, untracedListener) 56 | future.addListener( 57 | { 58 | try { 59 | val value = 60 | try { 61 | future.get() 62 | } catch (error: Throwable) { 63 | listener.onFailure(error) 64 | return@addListener 65 | } 66 | try { 67 | listener.onSuccess(value) 68 | } catch (error: Throwable) { 69 | listener.onFailure(CompletionException(error)) 70 | } 71 | } catch (t: Throwable) { 72 | failedInErrorHandler(t) 73 | } 74 | }, 75 | executor) 76 | } 77 | 78 | override fun subscribe(future: CompletionStage, untracedListener: RxListener) { 79 | val listener = Rx.tracingPolicy.hook(future, untracedListener) 80 | future.whenCompleteAsync( 81 | { value: T, exception: Throwable? -> 82 | try { 83 | if (exception == null) { 84 | try { 85 | listener.onSuccess(value) 86 | } catch (t: Throwable) { 87 | listener.onFailure(CompletionException(t)) 88 | } 89 | } else { 90 | listener.onFailure(exception) 91 | } 92 | } catch (t: Throwable) { 93 | failedInErrorHandler(t) 94 | } 95 | }, 96 | executor) 97 | } 98 | 99 | override fun subscribeDisposable(flow: Flow, untracedListener: RxListener): Job { 100 | val listener = Rx.tracingPolicy.hook(flow, untracedListener) 101 | return flow 102 | .onEach(listener.onValue::accept) 103 | .onCompletion { 104 | if (it != null && it !is CancellationException) { 105 | listener.onTerminate.accept(Optional.of(it)) 106 | } else listener.onTerminate.accept(Optional.empty()) 107 | } 108 | .launchIn(CoroutineScope(dispatcher)) 109 | } 110 | 111 | override fun subscribeDisposable( 112 | deferred: Deferred, 113 | untracedListener: RxListener 114 | ): Job { 115 | val listener = Rx.tracingPolicy.hook(deferred, untracedListener) 116 | return CoroutineScope(dispatcher).launch { 117 | try { 118 | listener.onSuccess(deferred.await()) 119 | } catch (e: Throwable) { 120 | listener.onFailure(e) 121 | } 122 | } 123 | } 124 | 125 | override fun subscribeDisposable( 126 | future: ListenableFuture, 127 | untracedListener: RxListener 128 | ): Job { 129 | val listener = Rx.tracingPolicy.hook(future, untracedListener) 130 | val job = Job() 131 | 132 | future.addListener( 133 | { 134 | try { 135 | if (!job.isCancelled) { 136 | val value = 137 | try { 138 | future.get() 139 | } catch (error: Throwable) { 140 | listener.onFailure(error) 141 | return@addListener 142 | } 143 | try { 144 | if (!job.isCancelled) { 145 | listener.onSuccess(value) 146 | } 147 | } catch (error: Throwable) { 148 | listener.onFailure(CompletionException(error)) 149 | } 150 | } 151 | } catch (t: Throwable) { 152 | failedInErrorHandler(t) 153 | } 154 | }, 155 | executor) 156 | return job 157 | } 158 | 159 | override fun subscribeDisposable( 160 | future: CompletionStage, 161 | untracedListener: RxListener 162 | ): Job { 163 | val listener = Rx.tracingPolicy.hook(future, untracedListener) 164 | val job = Job() 165 | 166 | future.whenCompleteAsync( 167 | { value: T, exception: Throwable? -> 168 | try { 169 | if (!job.isCancelled) { 170 | if (exception == null) { 171 | try { 172 | listener.onSuccess(value) 173 | } catch (t: Throwable) { 174 | listener.onFailure(CompletionException(t)) 175 | } 176 | } else { 177 | listener.onFailure(exception) 178 | } 179 | } 180 | } catch (t: Throwable) { 181 | failedInErrorHandler(t) 182 | } 183 | }, 184 | executor) 185 | return job 186 | } 187 | 188 | private fun failedInErrorHandler(t: Throwable) { 189 | Errors.log().accept(Error("Error handler threw error", t)) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/RxGetter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.base.Box 19 | import com.diffplug.common.rx.Rx.subscribe 20 | import java.util.function.BiFunction 21 | import java.util.function.Function 22 | import java.util.function.Supplier 23 | import kotlinx.coroutines.flow.Flow 24 | import kotlinx.coroutines.flow.combine 25 | import kotlinx.coroutines.flow.distinctUntilChanged 26 | import kotlinx.coroutines.flow.map 27 | 28 | /** 29 | * Represents a value which can be accessed through a traditional `get()` method or by listening to 30 | * its [io.reactivex.Observable]. 31 | * 32 | * `RxGetter`'s `Observable` has the semantics of a [io.reactivex.subjects.BehaviorSubject], meaning 33 | * that as soon as a listener subscribes to the `Observable`, it will emit the current value. 34 | * 35 | * Any time the value changes, `RxGetter`'s `Observable` will notify of the change. If the value did 36 | * not change (e.g. a field is set to its current value, which produces no change) then the 37 | * `Observable` will not fire. 38 | */ 39 | interface RxGetter : IFlowable, Supplier { 40 | /** 41 | * Maps an `RxGetter` to a new `RxGetter` by applying the `mapper` function to all of its values. 42 | * 43 | * If the `Observable` of the source `RxGetter` changes, but the `Function, R> mapper` 44 | * collapses these values to produce no change, then the mapped `Observable` shall not emit a new 45 | * value. 46 | * * Incorrect: `("A", "B", "C") -> map(String::length) = (1, 1, 1)` 47 | * * Correct: `("A", "B", "C") -> map(String::length) = (1)` 48 | */ 49 | fun map(mapper: Function): RxGetter { 50 | val src = this 51 | val mapped = src.asFlow().map { t: T -> mapper.apply(t) } 52 | val observable = mapped.distinctUntilChanged() 53 | return object : RxGetter { 54 | override fun asFlow(): Flow { 55 | return observable 56 | } 57 | 58 | override fun get(): R { 59 | return mapper.apply(src.get()) 60 | } 61 | } 62 | } 63 | 64 | companion object { 65 | /** 66 | * Creates an `RxGetter` from the given `Observable` and `initialValue`, appropriate for 67 | * observables which emit values on a single thread. 68 | * 69 | * The value returned by [RxGetter.get] will be the last value emitted by the observable, as 70 | * recorded by a non-volatile field. 71 | */ 72 | @JvmStatic 73 | fun from(observable: Flow, initialValue: T): RxGetter { 74 | val box = Box.of(initialValue) 75 | subscribe(observable) { value: T -> box.set(value) } 76 | return object : RxGetter { 77 | override fun asFlow(): Flow { 78 | return observable 79 | } 80 | 81 | override fun get(): T { 82 | return box.get() 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * Creates an `RxGetter` which combines two `RxGetter`s using the `BiFunction combine`. 89 | * 90 | * As with [.map], the observable only emits a new value if its value has changed. 91 | */ 92 | @JvmStatic 93 | fun combineLatest( 94 | t: RxGetter, 95 | u: RxGetter, 96 | combine: BiFunction 97 | ): RxGetter { 98 | val result: Flow = t.asFlow().combine(u.asFlow()) { a, b -> combine.apply(a, b) } 99 | return from(result.distinctUntilChanged(), combine.apply(t.get(), u.get())) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/RxListener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2025 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.base.Errors 19 | import com.diffplug.common.util.concurrent.FutureCallback 20 | import java.util.* 21 | import java.util.function.Consumer 22 | 23 | class RxListener 24 | internal constructor( 25 | internal val onValue: Consumer, 26 | internal val onTerminate: Consumer> 27 | ) : FutureCallback { 28 | override fun onSuccess(result: T?) { 29 | onValue.accept(result as T) 30 | onTerminate.accept(Optional.empty()) 31 | } 32 | 33 | override fun onFailure(e: Throwable) { 34 | onTerminate.accept(Optional.of(e)) 35 | } 36 | 37 | fun onErrorDontLog(e: Throwable) { 38 | if (onTerminate === logErrors) { 39 | return 40 | } else { 41 | val optError = Optional.of(e) 42 | if (onTerminate is DefaultTerminate) { 43 | onTerminate.onTerminate.accept(optError) 44 | } else { 45 | onTerminate.accept(optError) 46 | } 47 | } 48 | } 49 | 50 | val isLogging: Boolean 51 | /** Returns true iff the given Rx is a logging Rx. */ 52 | get() = onTerminate === logErrors || onTerminate is DefaultTerminate 53 | 54 | /** An error listener which promises to pass log all errors, without requiring the user to. */ 55 | internal open class DefaultTerminate(onTerminate: Consumer>) : 56 | Consumer> { 57 | internal val onTerminate: Consumer> = Objects.requireNonNull(onTerminate) 58 | 59 | override fun accept(t: Optional) { 60 | onTerminate.accept(t) 61 | if (t.isPresent) { 62 | logErrors.accept(t) 63 | } 64 | } 65 | } 66 | 67 | companion object { 68 | val logErrors: Consumer> = Consumer { error: Optional -> 69 | if (error.isPresent) { 70 | Errors.log().accept(error.get()) 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/RxLockBox.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.base.Converter 19 | import com.diffplug.common.rx.Rx.subscribe 20 | import java.util.function.Function 21 | import kotlinx.coroutines.flow.Flow 22 | import kotlinx.coroutines.flow.map 23 | 24 | /** [RxBox] and [LockBox] in one. */ 25 | interface RxLockBox : LockBox, RxBox { 26 | /** RxLockBox must map to another kind of LockBox. */ 27 | override fun map(converter: Converter): RxLockBox = 28 | RxLockBoxImp.Mapped(this, converter) 29 | 30 | override fun enforce(enforcer: Function): RxLockBox { 31 | // this must be a plain-old observable, because it needs to fire 32 | // every time an invariant is violated, not only when a violation 33 | // of the invariant causes a change in the output 34 | val mapped: Flow = asFlow().map { t: T -> enforcer.apply(t) } 35 | subscribe(mapped) { value: T -> this.set(value) } 36 | // now we can return the RxBox 37 | return map(Converter.from(enforcer, enforcer)) 38 | } 39 | 40 | companion object { 41 | /** Creates an `RxLockBox` containing the given value, which uses itself as the lock. */ 42 | @JvmStatic fun of(value: T): RxLockBox = RxLockBoxImp(value) 43 | 44 | /** Creates an `RxLockBox` containing the given value, which uses `lock` as the lock. */ 45 | @JvmStatic fun of(value: T, lock: Any): RxLockBox = RxLockBoxImp(value, lock) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/RxLockBoxImp.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.base.Box 19 | import com.diffplug.common.base.Converter 20 | import java.util.function.Function 21 | import kotlinx.coroutines.flow.Flow 22 | import kotlinx.coroutines.flow.MutableStateFlow 23 | import kotlinx.coroutines.flow.distinctUntilChanged 24 | import kotlinx.coroutines.flow.map 25 | 26 | internal class RxLockBoxImp : LockBoxImp, RxLockBox { 27 | val flow: MutableStateFlow 28 | 29 | constructor(value: T) : super(value) { 30 | flow = MutableStateFlow(value) 31 | } 32 | 33 | constructor(value: T, lock: Any) : super(value, lock) { 34 | flow = MutableStateFlow(value) 35 | } 36 | 37 | override fun set(newValue: T) { 38 | synchronized(lock()) { 39 | if (newValue != value) { 40 | value = newValue 41 | flow.value = newValue 42 | } 43 | } 44 | } 45 | 46 | override fun asFlow(): Flow = flow 47 | 48 | override fun toString(): String = "RxLockBox.of[" + get() + "]" 49 | 50 | internal class Mapped(delegate: RxLockBox, converter: Converter) : 51 | MappedImp>(delegate, converter), RxLockBox { 52 | val flow: Flow 53 | 54 | init { 55 | val mapped = delegate.asFlow().map { a: T -> converter.convertNonNull(a) } 56 | flow = mapped.distinctUntilChanged() 57 | } 58 | 59 | override fun lock(): Any = delegate.lock() 60 | 61 | override fun asFlow(): Flow = flow 62 | 63 | override fun modify(mutator: Function): R { 64 | val result = Box.Nullable.ofNull() 65 | delegate.modify { input: T -> 66 | val unmappedResult = mutator.apply(converter.convertNonNull(input)) 67 | result.set(unmappedResult) 68 | converter.revertNonNull(unmappedResult) 69 | } 70 | return result.get() 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/RxOrderedSet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2022 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.base.Preconditions; 20 | import com.diffplug.common.base.Unhandled; 21 | import java.util.ArrayList; 22 | import java.util.Collections; 23 | import java.util.HashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.Objects; 27 | import java.util.function.Consumer; 28 | 29 | /** 30 | * {@link RxBox}<{@link List}<T>> 31 | * which promises to exclude duplicates. 32 | * 33 | * We dont' actually want it to be this - we actually want it to be a 34 | * stateful wrapper around calls to `set`, but we'll settle for this 35 | * for now. 36 | */ 37 | public class RxOrderedSet extends ForwardingBox.Rx> { 38 | /** Creates an RxList with an initially empty value. */ 39 | 40 | public static RxOrderedSet ofEmpty() { 41 | return of(Collections.emptyList()); 42 | } 43 | 44 | /** Creates an RxList with an initially empty value. */ 45 | public static RxOrderedSet ofEmpty(OnDuplicate duplicatePolicy) { 46 | return of(Collections.emptyList(), duplicatePolicy); 47 | } 48 | 49 | /** Creates an RxList with the given initial value. */ 50 | public static RxOrderedSet of(List initial) { 51 | return of(initial, OnDuplicate.ERROR); 52 | } 53 | 54 | /** Creates an RxList with the given initial value. */ 55 | public static RxOrderedSet of(List initial, OnDuplicate duplicatePolicy) { 56 | return new RxOrderedSet(initial, duplicatePolicy); 57 | } 58 | 59 | /** Initally holds the given collection. */ 60 | protected RxOrderedSet(List initial, OnDuplicate duplicatePolicy) { 61 | super(RxBox.of(filter(initial, duplicatePolicy))); 62 | this.policy = duplicatePolicy; 63 | } 64 | 65 | private final OnDuplicate policy; 66 | 67 | // @formatter:off 68 | /** Policies for disallowing duplicates. */ 69 | public enum OnDuplicate { 70 | /** Throws an error when a duplicate is encountered. */ 71 | ERROR, 72 | /** Resolve duplicates by taking the first duplicate in the list. */ 73 | TAKE_FIRST, 74 | /** Resolve duplicates by taking the last duplicate in the list. */ 75 | TAKE_LAST 76 | } 77 | // @formatter:on 78 | 79 | /** Returns the duplicate policy for this RxList. */ 80 | public OnDuplicate getDuplicatePolicy() { 81 | return policy; 82 | } 83 | 84 | /** Sets the selection. */ 85 | @Override 86 | public void set(List newSelection) { 87 | Preconditions.checkNotNull(newSelection); 88 | if (!get().equals(newSelection)) { 89 | // the selection changed, so we will check it for duplicates 90 | newSelection = filter(newSelection, policy); 91 | // if it's still different than we expect... 92 | super.set(newSelection); 93 | } 94 | } 95 | 96 | private static List filter(List newList, OnDuplicate policy) { 97 | Objects.requireNonNull(policy); 98 | Map indexToTake = new HashMap<>(); 99 | 100 | // put all of the new values into the newList 101 | boolean hasDuplicate = false; 102 | indexToTake.clear(); 103 | for (int i = 0; i < newList.size(); ++i) { 104 | T item = newList.get(i); 105 | Integer previous = indexToTake.put(item, i); 106 | 107 | if (previous != null) { 108 | hasDuplicate = true; 109 | switch (policy) { 110 | case ERROR: 111 | throw new IllegalArgumentException("Item " + item + " is a duplicate!"); 112 | case TAKE_FIRST: 113 | // we'll keep the one that used to be there 114 | indexToTake.put(item, previous); 115 | break; 116 | case TAKE_LAST: 117 | // we'll keep the element that we just set in the map 118 | break; 119 | default: 120 | throw Unhandled.enumException(policy); 121 | } 122 | } 123 | } 124 | 125 | if (!hasDuplicate) { 126 | // if there wasn't a duplicate, then there's no change necessary 127 | return newList; 128 | } else { 129 | List noDuplicates = new ArrayList<>(indexToTake.size()); 130 | for (int i = 0; i < newList.size(); ++i) { 131 | T value = newList.get(i); 132 | if (indexToTake.get(value) == i) { 133 | // if we're supposed to take this value, then take it! 134 | noDuplicates.add(value); 135 | } 136 | } 137 | // returns the filtered list 138 | return noDuplicates; 139 | } 140 | } 141 | 142 | /** Mutates this set. */ 143 | public List mutate(Consumer> mutator) { 144 | List mutated = new ArrayList<>(get()); 145 | mutator.accept(mutated); 146 | set(mutated); 147 | return mutated; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/RxSubscriber.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2021 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.rx.Rx.TrackCancelled 19 | import com.diffplug.common.rx.Rx.onValue 20 | import com.diffplug.common.rx.Rx.onValueOnTerminate 21 | import com.diffplug.common.util.concurrent.ListenableFuture 22 | import java.util.concurrent.CompletionStage 23 | import java.util.function.Consumer 24 | import kotlinx.coroutines.Deferred 25 | import kotlinx.coroutines.Job 26 | import kotlinx.coroutines.flow.Flow 27 | 28 | interface RxSubscriber { 29 | fun subscribe(flow: Flow, listener: RxListener) 30 | 31 | fun subscribe(deferred: Deferred, listener: RxListener) 32 | 33 | fun subscribe(future: ListenableFuture, listener: RxListener) 34 | 35 | fun subscribe(future: CompletionStage, listener: RxListener) 36 | 37 | fun subscribe(flow: Flow, listener: Consumer) { 38 | subscribe(flow, onValue(listener)) 39 | } 40 | 41 | fun subscribe(deferred: Deferred, listener: Consumer) { 42 | subscribe(deferred, onValue(listener)) 43 | } 44 | 45 | fun subscribe(flow: IFlowable, listener: RxListener) { 46 | subscribe(flow.asFlow(), listener) 47 | } 48 | 49 | fun subscribe(flow: IFlowable, listener: Consumer) { 50 | subscribe(flow, onValue(listener)) 51 | } 52 | 53 | fun subscribe(future: ListenableFuture, listener: Consumer) { 54 | subscribe(future, onValueOnTerminate(listener, TrackCancelled(future))) 55 | } 56 | 57 | fun subscribe(future: CompletionStage, listener: Consumer) { 58 | subscribe(future, onValueOnTerminate(listener, TrackCancelled(future.toCompletableFuture()))) 59 | } 60 | 61 | fun subscribeDisposable(flow: Flow, listener: RxListener): Job 62 | 63 | fun subscribeDisposable(deferred: Deferred, listener: RxListener): Job 64 | 65 | fun subscribeDisposable(future: ListenableFuture, listener: RxListener): Job 66 | 67 | fun subscribeDisposable(future: CompletionStage, listener: RxListener): Job 68 | 69 | fun subscribeDisposable(flow: Flow, listener: Consumer): Job { 70 | return subscribeDisposable(flow, onValue(listener)) 71 | } 72 | 73 | fun subscribeDisposable(deferred: Deferred, listener: Consumer): Job { 74 | return subscribeDisposable(deferred, onValue(listener)) 75 | } 76 | 77 | fun subscribeDisposable(flow: IFlowable, listener: RxListener): Job { 78 | return subscribeDisposable(flow.asFlow(), listener) 79 | } 80 | 81 | fun subscribeDisposable(flow: IFlowable, listener: Consumer): Job { 82 | return subscribeDisposable(flow, onValue(listener)) 83 | } 84 | 85 | fun subscribeDisposable(future: ListenableFuture, listener: Consumer): Job { 86 | return subscribeDisposable(future, onValueOnTerminate(listener, TrackCancelled(future))) 87 | } 88 | 89 | fun subscribeDisposable(future: CompletionStage, listener: Consumer): Job { 90 | return subscribeDisposable( 91 | future, onValueOnTerminate(listener, TrackCancelled(future.toCompletableFuture()))) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/RxTracingPolicy.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2025 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx 17 | 18 | import com.diffplug.common.base.Errors 19 | import com.diffplug.common.rx.Rx.onValueOnTerminate 20 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings 21 | import java.util.* 22 | import java.util.function.BiPredicate 23 | import java.util.function.Consumer 24 | 25 | /** 26 | * Plugin which gets notified of every call to [Rx.subscribe], allowing various kinds of tracing. 27 | * 28 | * By default, no tracing is done. To enable tracing, do one of the following: 29 | * * Execute this at the very beginning of your application: 30 | * `DurianPlugins.set(RxTracingPolicy.class, new MyTracingPolicy());` 31 | * * Set this system property: 32 | * `durian.plugins.com.diffplug.common.rx.RxTracingPolicy=fully.qualified.name.to.MyTracingPolicy` 33 | * 34 | * [LogDisposableTrace] is a useful tracing policy for debugging errors within callbacks. 35 | * 36 | * @see DurianPlugins 37 | */ 38 | interface RxTracingPolicy { 39 | /** 40 | * Given an observable, and an [Rx] which is about to be subscribed to this observable, return a 41 | * (possibly instrumented) `Rx`. 42 | * 43 | * @param observable The [IFlowable], [Observable], or [ListenableFuture] which is about to be 44 | * subscribed to. 45 | * @param listener The [Rx] which is about to be subscribed. 46 | * @return An [Rx] which may (or may not) be instrumented. To ensure that the program's behavior 47 | * is not changed, implementors should ensure that all method calls are delegated unchanged to 48 | * the original listener eventually. 49 | */ 50 | fun hook(flow: Any, listener: RxListener): RxListener 51 | 52 | /** 53 | * An [RxTracingPolicy] which logs the stack trace of every subscription, so that it can decorate 54 | * any exceptions with the stack trace at the time they were subscribed. 55 | * 56 | * This logging is fairly expensive, so you might want to set the [LogDisposableTrace.shouldLog] 57 | * field, which determines whether a subscription is logged or passed along untouched. 58 | * 59 | * By default every [Rx.onValue] listener will be logged, but nothing else. 60 | * 61 | * To enable this tracing policy, do one of the following: 62 | * * Execute this at the very beginning of your application: 63 | * `DurianPlugins.set(RxTracingPolicy.class, new LogDisposableTrace());` 64 | * * Set this system property: 65 | * `durian.plugins.com.diffplug.common.rx.RxTracingPolicy=com.diffplug.common.rx.RxTracingPolicy$LogDisposableTrace` 66 | * 67 | * @see 68 | * [LogSubscriptionTrace source code](https://github.com/diffplug/durian-rx/blob/master/src/com/diffplug/common/rx/RxTracingPolicy.java?ts=4) 69 | * @see DurianPlugins 70 | */ 71 | class LogSubscriptionTrace : RxTracingPolicy { 72 | override fun hook(flow: Any, listener: RxListener): RxListener { 73 | if (!shouldLog.test(flow, listener)) { 74 | // we're not logging, so pass the listener unchanged 75 | return listener 76 | } else { 77 | // capture the stack at the time of the subscription 78 | val subscriptionTrace = 79 | StackDumper.captureStackBelow( 80 | LogSubscriptionTrace::class.java, RxExecutor::class.java, Rx::class.java) 81 | // create a new Rx which passes values unchanged, but instruments exceptions with the 82 | // subscription stack 83 | return onValueOnTerminate( 84 | listener.onValue, KotlinOnTerminateBridge(listener, subscriptionTrace)) 85 | } 86 | } 87 | 88 | internal class KotlinOnTerminateBridge( 89 | val listener: RxListener<*>, 90 | val subscriptionTrace: List 91 | ) : Consumer> { 92 | override fun accept(error: Optional) { 93 | if (error.isPresent) { 94 | // if there is an error, wrap it in a SubscriptionException and log it 95 | val subException = SubscriptionException(error.get(), subscriptionTrace) 96 | Errors.log().accept(subException) 97 | // if the original listener was just logging exceptions, there's no need to notify it, as 98 | // this would be a double-log 99 | if (!listener.isLogging) { 100 | // the listener isn't a simple logger, so we should pass the original exception 101 | // to ensure that our logging doesn't change the program's behavior 102 | listener.onTerminate.accept(Optional.of(error.get())) 103 | } 104 | } else { 105 | // pass clean terminations unchanged 106 | listener.onTerminate.accept(Optional.empty()) 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * An Exception which has the stack trace of the Rx.subscription() call which created the 113 | * subscription in which the cause was thrown. 114 | */ 115 | internal class SubscriptionException(cause: Throwable, stack: List) : 116 | Exception(cause) { 117 | init { 118 | stackTrace = stack.toTypedArray() 119 | } 120 | 121 | companion object { 122 | private const val serialVersionUID = 1L 123 | } 124 | } 125 | 126 | companion object { 127 | /** 128 | * The BiPredicate which determines which subscriptions should be logged. By default, any Rx 129 | * which is logging will be logged. 130 | */ 131 | @SuppressFBWarnings( 132 | value = ["MS_SHOULD_BE_FINAL"], 133 | justification = "This is public on purpose, and is only functional in a debug mode.") 134 | var shouldLog: BiPredicate> = 135 | BiPredicate { flow: Any?, listener: RxListener<*> -> 136 | listener.isLogging 137 | } 138 | } 139 | } 140 | 141 | companion object { 142 | /** An `RxTracingPolicy` which performs no tracing, and has very low overhead. */ 143 | val NONE: RxTracingPolicy = 144 | object : RxTracingPolicy { 145 | override fun hook(flow: Any, listener: RxListener): RxListener = listener 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/StackDumper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.base.StringPrinter; 20 | import java.io.PrintStream; 21 | import java.util.ArrayList; 22 | import java.util.Arrays; 23 | import java.util.List; 24 | import java.util.ListIterator; 25 | import java.util.Objects; 26 | import java.util.function.Predicate; 27 | import java.util.stream.Collectors; 28 | 29 | /** 30 | * Copied from DurianDebug. 31 | * 32 | * Utility methods for dumping the stack - arbitrarily or at specific trigger points (such as when a certain string prints to console). 33 | *

34 | * If someone is printing "junk" to the console and you can't figure out why, try {@code StackDumper.dumpWhenSysOutContains("junk")}. 35 | */ 36 | class StackDumper { 37 | static StringPrinter pristineSysErr = new StringPrinter(System.err::print); 38 | 39 | /** Dumps the given message and stack to the system error console. */ 40 | public static void dump(String message, List stack) { 41 | Objects.requireNonNull(message); 42 | printEmphasized(message + "\n" + stackTraceToString(stack)); 43 | } 44 | 45 | /** Dumps the given message and stack to the system error console. */ 46 | public static void dump(String message, StackTraceElement[] stackTrace) { 47 | dump(message, Arrays.asList(stackTrace)); 48 | } 49 | 50 | /** Dumps the given message and exception stack to the system error console */ 51 | public static void dump(String message, Throwable exception) { 52 | printEmphasized(StringPrinter.buildString(printer -> { 53 | printer.println(message); 54 | exception.printStackTrace(printer.toPrintWriter()); 55 | })); 56 | } 57 | 58 | /** Dumps the current stack to the system error console. */ 59 | public static void dump(String message) { 60 | dump(message, captureStackBelow()); 61 | } 62 | 63 | /** Dumps the first {@code stackLimit} frames of the current stack to the system error console, excluding traces from {@code classPrefixesToExclude}. */ 64 | public static void dump(String message, int stackLimit, String... classPrefixesToExclude) { 65 | Objects.requireNonNull(message); 66 | // class names to include 67 | Predicate isIncluded = trace -> { 68 | for (String prefix : classPrefixesToExclude) { 69 | if (trace.getClassName().startsWith(prefix)) { 70 | return false; 71 | } 72 | } 73 | return true; 74 | }; 75 | // filter the stack 76 | List stack = captureStackBelow().stream() 77 | .filter(trace -> trace.getLineNumber() >= 0) 78 | .filter(isIncluded) 79 | .limit(stackLimit) 80 | .collect(Collectors.toList()); 81 | dump(message, stack); 82 | } 83 | 84 | /** Dumps a stack trace anytime the trigger string is printed to System.out. */ 85 | public static void dumpWhenSysOutContains(String trigger) { 86 | System.setOut(wrapAndDumpWhenContains(System.out, trigger)); 87 | } 88 | 89 | /** Dumps a stack trace anytime trigger string is printed to System.err. */ 90 | public static void dumpWhenSysErrContains(String trigger) { 91 | System.setErr(wrapAndDumpWhenContains(System.err, trigger)); 92 | } 93 | 94 | /** 95 | * Returns a PrintStream which will redirect all of its output to the source PrintStream. If 96 | * the trigger string is passed through the wrapped PrintStream, then it will dump the 97 | * stack trace of the call that printed the trigger. 98 | * 99 | * @param source 100 | * the returned PrintStream will delegate to this stream 101 | * @param trigger 102 | * the string which triggers a stack dump 103 | * @return a PrintStream with the above properties 104 | */ 105 | public static PrintStream wrapAndDumpWhenContains(PrintStream source, String trigger) { 106 | Objects.requireNonNull(source); 107 | Objects.requireNonNull(trigger); 108 | StringPrinter wrapped = new StringPrinter(StringPrinter.stringsToLines(perLine -> { 109 | source.println(perLine); 110 | if (perLine.contains(trigger)) { 111 | dump("Triggered by " + trigger); 112 | } 113 | })); 114 | return wrapped.toPrintStream(); 115 | } 116 | 117 | /** Converts a list of stack trace elements to a String similar to Throwable.printStackTrace(). */ 118 | private static String stackTraceToString(List stack) { 119 | return StringPrinter.buildString(printer -> { 120 | for (StackTraceElement element : stack) { 121 | printer.print("at "); 122 | printer.print(element.getClassName()); 123 | printer.print("."); 124 | printer.print(element.getMethodName()); 125 | printer.print("("); 126 | printer.print(element.getFileName()); 127 | printer.print(":"); 128 | printer.print(Integer.toString(element.getLineNumber())); 129 | printer.println(")"); 130 | } 131 | }); 132 | } 133 | 134 | /** Captures all of the current stack which is below the given classes. */ 135 | public static List captureStackBelow(Class... clazzes) { 136 | List> toIgnore = new ArrayList<>(clazzes.length + 1); 137 | toIgnore.addAll(Arrays.asList(clazzes)); 138 | toIgnore.add(StackDumper.class); 139 | 140 | Predicate isSkipped = element -> toIgnore.stream().anyMatch(clazz -> { 141 | String name = element.getClassName(); 142 | return name.equals(clazz.getName()) || name.startsWith(clazz.getName() + "$$Lambda"); 143 | }); 144 | 145 | List rawStack = Arrays.asList(Thread.currentThread().getStackTrace()); 146 | ListIterator iterator = rawStack.listIterator(); 147 | 148 | // iterate until we find something skipped 149 | while (iterator.hasNext() && !isSkipped.test(iterator.next())) {} 150 | 151 | boolean foundSomethingToSkip = iterator.hasNext(); 152 | if (foundSomethingToSkip) { 153 | // iterate unti we find something not skipped 154 | while (iterator.hasNext() && isSkipped.test(iterator.next())) {} 155 | // the filtering was successful! 156 | return rawStack.subList(iterator.previousIndex(), rawStack.size()); 157 | } else { 158 | // we didn't find something to skip, so we'll return the whole stack 159 | return rawStack; 160 | } 161 | } 162 | 163 | /** 164 | * Prints the given string to the the given printer, wrapped in hierarchy-friendly 165 | * braces. Useful for emphasizing a specific event from a sea of logging statements. 166 | */ 167 | private static void printEmphasized(String toPrint) { 168 | // print the triggered header 169 | pristineSysErr.println("+----------\\"); 170 | for (String line : toPrint.split("\n")) { 171 | pristineSysErr.println("| " + line); 172 | } 173 | pristineSysErr.println("+----------/"); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/common/rx/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * DurianRx unifies RxJava's [Observable](http://reactivex.io/documentation/observable.html) with Guava's [ListenableFuture](https://code.google.com/p/guava-libraries/wiki/ListenableFutureExplained). If you happen to be using SWT as a widget toolkit, then you'll want to look at [DurianSwt](https://github.com/diffplug/durian-swt) as well. 3 | * 4 | * ```java 5 | * Observable observable = someObservable(); 6 | * ListenableFuture future = someFuture(); 7 | * Rx.subscribe(observable, val -> doSomething(val)); 8 | * Rx.subscribe(future, val -> doSomething(val)); 9 | * ``` 10 | * 11 | * It also provides {@linkplain RxGetter reactive getters}, a simple abstraction for piping data which allows access via `T get()` or `Observable asObservable()`. 12 | * 13 | * ```java 14 | * RxBox mousePos = RxBox.of(new Point(0, 0)); 15 | * this.addMouseListener(e -> mousePos.set(new Point(e.x, e.y))); 16 | * 17 | * Rectangle hotSpot = new Rectangle(0, 0, 10, 10) 18 | * RxGetter isMouseOver = mousePos.map(hotSpot::contains); 19 | * ``` 20 | * 21 | * Debugging an error which involves lots of callbacks can be difficult. To make this easier, DurianRx includes a {@linkplain RxTracingPolicy tracing capability}, which makes this task easier. 22 | * 23 | * ```java 24 | * // anytime an error is thrown in an Rx callback, the stack trace of the error 25 | * // will be wrapped by the stack trace of the original subscription 26 | * DurianPlugins.set(RxTracingPolicy.class, new LogDisposableTrace()). 27 | * ``` 28 | */ 29 | 30 | @ParametersAreNonnullByDefault 31 | package com.diffplug.common.rx; 32 | 33 | 34 | import javax.annotation.ParametersAreNonnullByDefault; 35 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/CasBoxTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.base.Converter; 20 | import com.diffplug.common.base.Errors; 21 | import com.diffplug.common.debug.ThreadHarness; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | import java.util.function.Consumer; 24 | import org.junit.Assert; 25 | import org.junit.Test; 26 | 27 | public class CasBoxTest { 28 | @Test 29 | public void testModifyIsLocked() { 30 | testRetryingBehavior(box -> box.modify(val -> val + "3"), 2, "132"); 31 | } 32 | 33 | @Test 34 | public void testSetIsLocked() { 35 | testRetryingBehavior(box -> box.set("5"), 2, "52"); 36 | testRetryingBehavior(box -> { 37 | box.set("5"); 38 | Errors.log().run(() -> Thread.sleep(150)); 39 | box.set("4"); 40 | }, 3, "42"); 41 | } 42 | 43 | @Test 44 | public void testGetIsLocked() { 45 | testRetryingBehavior(box -> box.get(), 1, "12"); 46 | } 47 | 48 | static void testRetryingBehavior(Consumer> timed, int numTimesCalled, String expectedResult) { 49 | testRetryingBehavior(CasBox.of("1"), timed, numTimesCalled, expectedResult); 50 | } 51 | 52 | @Test 53 | public void testMappedModifyIsLocked() { 54 | testMappedRetryingBehavior(box -> box.modify(val -> val + "3"), 2, "132"); 55 | } 56 | 57 | @Test 58 | public void testMappedSetIsLocked() { 59 | testMappedRetryingBehavior(box -> box.set("5"), 2, "52"); 60 | testMappedRetryingBehavior(box -> { 61 | box.set("5"); 62 | Errors.log().run(() -> Thread.sleep(150)); 63 | box.set("4"); 64 | }, 3, "42"); 65 | } 66 | 67 | @Test 68 | public void testMappedGetIsLocked() { 69 | testMappedRetryingBehavior(box -> box.get(), 1, "12"); 70 | } 71 | 72 | static void testMappedRetryingBehavior(Consumer> timed, int numTimesCalled, String expectedResult) { 73 | CasBox box = CasBox.of(1); 74 | testRetryingBehavior(box.map(intConverter()), timed, numTimesCalled, expectedResult); 75 | } 76 | 77 | static void testRetryingBehavior(CasBox box, Consumer> timed, int numTimesCalled, String expectedResult) { 78 | AtomicInteger timesCalled = new AtomicInteger(); 79 | ThreadHarness harness = new ThreadHarness(); 80 | harness.add(() -> { 81 | box.modify(val -> { 82 | timesCalled.incrementAndGet(); 83 | Errors.rethrow().run(() -> Thread.sleep(100)); 84 | return val + "2"; 85 | }); 86 | }); 87 | harness.add(() -> { 88 | timed.accept(box); 89 | }); 90 | harness.run(); 91 | // make sure it got called as many times as required 92 | Assert.assertEquals(numTimesCalled, timesCalled.get()); 93 | // make sure that the final result was what was expected 94 | Assert.assertEquals(expectedResult, box.get()); 95 | } 96 | 97 | static Converter intConverter() { 98 | return Converter.from(i -> Integer.toString(i), Integer::parseInt); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/ChitTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.base.Box; 20 | import org.junit.Assert; 21 | import org.junit.Test; 22 | 23 | public class ChitTest { 24 | @Test 25 | public void alreadyDisposed() { 26 | assertDisposedBehavior(Chit.alreadyDisposed()); 27 | } 28 | 29 | @Test 30 | public void settable() { 31 | Chit.Settable chit = Chit.settable(); 32 | Assert.assertFalse(chit.isDisposed()); 33 | 34 | Box hasBeenDisposed = Box.of(false); 35 | chit.runWhenDisposed(() -> hasBeenDisposed.set(true)); 36 | 37 | Assert.assertFalse(hasBeenDisposed.get()); 38 | chit.dispose(); 39 | Assert.assertTrue(hasBeenDisposed.get()); 40 | Assert.assertTrue(chit.isDisposed()); 41 | 42 | assertDisposedBehavior(chit); 43 | } 44 | 45 | private void assertDisposedBehavior(Chit chit) { 46 | Assert.assertTrue(chit.isDisposed()); 47 | Box hasBeenDisposed = Box.of(false); 48 | chit.runWhenDisposed(() -> hasBeenDisposed.set(true)); 49 | Assert.assertTrue(hasBeenDisposed.get()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/LockBoxTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.base.Box; 20 | import com.diffplug.common.base.Errors; 21 | import com.diffplug.common.debug.LapTimer; 22 | import com.diffplug.common.debug.ThreadHarness; 23 | import com.diffplug.common.primitives.Ints; 24 | import java.util.function.Consumer; 25 | import java.util.function.Function; 26 | import org.junit.Assert; 27 | import org.junit.Test; 28 | 29 | public class LockBoxTest { 30 | @Test 31 | public void testLockIdentity() { 32 | LockBox selfBox = LockBox.of(1); 33 | Assert.assertSame(selfBox, selfBox.lock()); 34 | 35 | Object otherLock = new Object(); 36 | LockBox otherBox = LockBox.of(1, otherLock); 37 | Assert.assertSame(otherLock, otherBox.lock()); 38 | } 39 | 40 | @Test 41 | public void testMappedLockIdentity() { 42 | LockBox intBox = LockBox.of(1); 43 | LockBox strBox = intBox.map(Ints.stringConverter().reverse()); 44 | Assert.assertSame(intBox.lock(), strBox.lock()); 45 | 46 | Object otherLock = new Object(); 47 | Assert.assertSame(otherLock, LockBox.of(1, otherLock).map(Ints.stringConverter().reverse()).lock()); 48 | } 49 | 50 | @Test 51 | public void testGetSetModify() { 52 | testLockingBehaviorGetSetModify("LockBox", LockBox::of); 53 | } 54 | 55 | @Test 56 | public void testMappedGetSetModify() { 57 | Function> constructor = initial -> { 58 | Integer initialInt = Integer.parseInt(initial); 59 | LockBox box = LockBox.of(initialInt); 60 | return box.map(Ints.stringConverter().reverse()); 61 | }; 62 | testLockingBehaviorGetSetModify("LockBox mapped", constructor); 63 | } 64 | 65 | static void testLockingBehaviorGetSetModify(String message, Function> constructor) { 66 | testLockingBehavior(message + " get", constructor, box -> box.get(), "12"); 67 | testLockingBehavior(message + " set", constructor, box -> box.set("5"), "5"); 68 | testLockingBehavior(message + " modify", constructor, box -> box.modify(val -> val + "3"), "123"); 69 | } 70 | 71 | static void testLockingBehavior(String message, Function> constructor, Consumer> timed, String expectedResult) { 72 | LockBox box = constructor.apply("1"); 73 | Box.Nullable elapsed = Box.Nullable.of(null); 74 | ThreadHarness harness = new ThreadHarness(); 75 | harness.add(() -> { 76 | box.modify(val -> { 77 | Errors.rethrow().run(() -> Thread.sleep(100)); 78 | return val + "2"; 79 | }); 80 | }); 81 | harness.add(() -> { 82 | LapTimer timer = LapTimer.createMs(); 83 | timed.accept(box); 84 | elapsed.set(timer.lap()); 85 | }); 86 | harness.run(); 87 | // make sure that the action which should have been delayed, was delayed for the proper time 88 | Assert.assertEquals("Wrong delay time for " + message, 0.1, elapsed.get().doubleValue(), 0.05); 89 | // make sure that the final result was what was expected 90 | Assert.assertEquals("Wrong result for " + message, expectedResult, box.get()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/OrderedLockTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2022 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.base.Errors; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.Random; 23 | import java.util.concurrent.atomic.AtomicInteger; 24 | import java.util.stream.Collectors; 25 | import java.util.stream.Stream; 26 | import org.junit.Test; 27 | 28 | public class OrderedLockTest { 29 | @Test 30 | public void takesAbout5Seconds() { 31 | int numLocks = 5; 32 | int numThreads = 20; 33 | int maxSinglePauseMs = 10; 34 | int maxNumIncrements = 1000; 35 | Random random = new Random(0); 36 | 37 | List locks = Stream.generate(Object::new) 38 | .limit(numLocks) 39 | .collect(Collectors.toList()); 40 | 41 | AtomicInteger numIncrements = new AtomicInteger(); 42 | 43 | List threads = Stream.generate(() -> { 44 | Thread thread = new Thread(() -> { 45 | int numIncrementsNow; 46 | do { 47 | // take a number of locks, up to (2 * numLocks) 48 | // possible to take a single lock multiple times 49 | int locksToTake = random.nextInt(2 * numLocks); 50 | List ourLocks = new ArrayList<>(locksToTake); 51 | for (int i = 0; i < locksToTake; ++i) { 52 | ourLocks.add(locks.get(random.nextInt(numLocks))); 53 | } 54 | 55 | // take the locks, and sleep a random amount before incrementing the count 56 | numIncrementsNow = OrderedLock.on(ourLocks).takeAndGet(() -> { 57 | Errors.rethrow().run(() -> { 58 | Thread.sleep(random.nextInt(maxSinglePauseMs)); 59 | }); 60 | return numIncrements.incrementAndGet(); 61 | }); 62 | } while (numIncrementsNow < maxNumIncrements); 63 | }, "TestThread"); 64 | thread.start(); 65 | return thread; 66 | }) 67 | .limit(numThreads) 68 | .collect(Collectors.toList()); 69 | 70 | // wait for the threads to finish 71 | for (Thread thread : threads) { 72 | Errors.rethrow().run(thread::join); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/PackageSanityTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2025 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | import com.diffplug.common.base.Consumers; 19 | import com.diffplug.common.testing.AbstractPackageSanityTests; 20 | import java.util.Collections; 21 | 22 | public class PackageSanityTests extends AbstractPackageSanityTests { 23 | public PackageSanityTests() { 24 | publicApiOnly(); 25 | ignoreClasses(Collections.singleton(RxExample.class)::contains); 26 | setDefault(RxListener.class, Rx.onValue(Consumers.doNothing())); 27 | setDefault(RxExecutor.class, Rx.sameThreadExecutor()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/RxAndListenableFutureSemantics.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2025 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | import com.diffplug.common.util.concurrent.SettableFuture; 19 | import java.util.Optional; 20 | import java.util.concurrent.CompletableFuture; 21 | import kotlinx.coroutines.flow.MutableStateFlow; 22 | import kotlinx.coroutines.flow.StateFlowKt; 23 | import org.junit.Test; 24 | 25 | /** 26 | * This is a simple little test for confirming the behavior of 27 | * subscribing to stuff. 28 | */ 29 | public class RxAndListenableFutureSemantics { 30 | @Test 31 | public void testBehaviorSubjectSubscribe() { 32 | // create an behavior subject, subscribe pre, and pump test through 33 | MutableStateFlow testSubject = StateFlowKt.MutableStateFlow("initial"); 34 | RxAsserter observer = RxAsserter.on(testSubject); 35 | // the observer gets the value immediately 36 | observer.assertValues("initial"); 37 | 38 | // call on next, and the observer gets the new value immediately 39 | testSubject.setValue("value"); 40 | observer.assertValues("initial", "value"); 41 | } 42 | 43 | @Test 44 | public void testListenableFutureCancellationResult() { 45 | SettableFuture future = SettableFuture.create(); 46 | 47 | // subscribe to a future then cancel should terminate with CancellationException 48 | RxAsserter assertDuring = RxAsserter.on(future); 49 | future.cancel(true); 50 | assertDuring.assertTerminalExceptionClass(java.util.concurrent.CancellationException.class); 51 | 52 | // subscribe to a cancelled future should terminate with CancellationException 53 | RxAsserter assertAfter = RxAsserter.on(future); 54 | assertAfter.assertTerminalExceptionClass(java.util.concurrent.CancellationException.class); 55 | } 56 | 57 | @Test 58 | public void testListenableFutureCancelAfterSet() { 59 | SettableFuture future = SettableFuture.create(); 60 | future.set("Some value"); 61 | 62 | // cancelling after setting value should not fail 63 | RxAsserter assertDuring = RxAsserter.on(future); 64 | future.cancel(true); 65 | assertDuring.assertTerminal(Optional.empty()); 66 | 67 | RxAsserter assertAfter = RxAsserter.on(future); 68 | assertAfter.assertTerminal(Optional.empty()); 69 | } 70 | 71 | @Test 72 | public void testCompletableFutureCancellationResult() { 73 | CompletableFuture future = new CompletableFuture<>(); 74 | 75 | // subscribe to a future then cancel should terminate with CancellationException 76 | RxAsserter assertDuring = RxAsserter.on(future); 77 | future.cancel(true); 78 | assertDuring.assertTerminalExceptionClass(java.util.concurrent.CancellationException.class); 79 | 80 | // subscribe to a cancelled future should terminate with CancellationException 81 | RxAsserter assertAfter = RxAsserter.on(future); 82 | assertAfter.assertTerminalExceptionClass(java.util.concurrent.CancellationException.class); 83 | } 84 | 85 | @Test 86 | public void testCompletableFutureCancelAfterSet() { 87 | CompletableFuture future = new CompletableFuture<>(); 88 | future.complete("Some value"); 89 | 90 | // subscribe to a future then cancel should terminate with CancellationException 91 | RxAsserter assertDuring = RxAsserter.on(future); 92 | future.cancel(true); 93 | assertDuring.assertTerminal(Optional.empty()); 94 | 95 | // subscribe to a cancelled future should terminate with CancellationException 96 | RxAsserter assertAfter = RxAsserter.on(future); 97 | assertAfter.assertTerminal(Optional.empty()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/RxApiJustification.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2025 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | import com.diffplug.common.util.concurrent.FutureCallback; 19 | import com.diffplug.common.util.concurrent.Futures; 20 | import com.diffplug.common.util.concurrent.ListenableFuture; 21 | import com.diffplug.common.util.concurrent.MoreExecutors; 22 | import com.diffplug.common.util.concurrent.SettableFuture; 23 | import org.junit.Test; 24 | 25 | /** The point of this test is to demonstrate why the Rx API should be what it is. */ 26 | @SuppressWarnings("null") 27 | public class RxApiJustification { 28 | /** Explains why Futures(static).addCallback is better than future(instance).addListener */ 29 | @Test(expected = NullPointerException.class) 30 | public void staticBetterThanInstanceForListenableFuture() { 31 | SettableFuture future = SettableFuture.create(); 32 | DpRxWithFutureRunnable listener = null; 33 | // static (clearly better) 34 | Futures.addCallback(future, listener); 35 | // instance (clearly worse) 36 | future.addListener(listener.toFutureRunnable(future), MoreExecutors.directExecutor()); 37 | } 38 | 39 | /** A theoretical DpRx with support for creating Runnables to add as listeners to ListenableFutures. */ 40 | private static class DpRxWithFutureRunnable implements FutureCallback { 41 | /** 42 | * Returns a Runnable appropriate for a FutureListener callback, e.g. 43 | * 44 | * future.addListener(listener.toFutureRunnable(future), executor); 45 | */ 46 | public Runnable toFutureRunnable(ListenableFuture future) { 47 | return () -> { 48 | try { 49 | T value = future.get(); 50 | onSuccess(value); 51 | } catch (Throwable t) { 52 | onFailure(t); 53 | } 54 | }; 55 | } 56 | 57 | @Override 58 | public void onSuccess(T result) {} 59 | 60 | @Override 61 | public void onFailure(Throwable t) {} 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/RxAsserter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2025 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | import com.diffplug.common.util.concurrent.ListenableFuture; 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | import java.util.Optional; 23 | import java.util.concurrent.CompletionStage; 24 | import kotlinx.coroutines.flow.Flow; 25 | import org.junit.Assert; 26 | 27 | final class RxAsserter { 28 | private final List values = new ArrayList<>(); 29 | private Optional terminal = null; 30 | private final RxListener listener = Rx.onValueOnTerminate(value -> { 31 | synchronized (RxAsserter.this) { 32 | values.add(value); 33 | } 34 | }, terminate -> { 35 | synchronized (RxAsserter.this) { 36 | terminal = terminate; 37 | } 38 | }); 39 | 40 | public static RxAsserter on(Flow observable) { 41 | RxAsserter asserter = new RxAsserter<>(); 42 | Rx.subscribe(observable, asserter.listener); 43 | return asserter; 44 | } 45 | 46 | public static RxAsserter on(IFlowable observable) { 47 | RxAsserter asserter = new RxAsserter<>(); 48 | Rx.subscribe(observable, asserter.listener); 49 | return asserter; 50 | } 51 | 52 | public static RxAsserter on(ListenableFuture observable) { 53 | RxAsserter asserter = new RxAsserter<>(); 54 | Rx.subscribe(observable, asserter.listener); 55 | return asserter; 56 | } 57 | 58 | public static RxAsserter on(CompletionStage observable) { 59 | RxAsserter asserter = new RxAsserter<>(); 60 | Rx.subscribe(observable, asserter.listener); 61 | return asserter; 62 | } 63 | 64 | /** Asserts that the given values were observed. */ 65 | @SafeVarargs 66 | public final void assertValues(T... expected) { 67 | synchronized (this) { 68 | Assert.assertEquals(Arrays.asList(expected), values); 69 | } 70 | } 71 | 72 | /** Asserts that the given terminal condition was observed. */ 73 | public void assertTerminal(Optional expected) { 74 | synchronized (this) { 75 | Assert.assertEquals(expected, terminal); 76 | } 77 | } 78 | 79 | /** Asserts that the given terminal condition was observed. */ 80 | public void assertTerminalExceptionClass(Class clazz) { 81 | synchronized (this) { 82 | Assert.assertTrue(terminal.isPresent()); 83 | Assert.assertEquals(clazz, terminal.get().getClass()); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/RxBoxTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.primitives.Ints; 20 | import java.util.function.Function; 21 | import org.junit.Test; 22 | 23 | public class RxBoxTest { 24 | @Test 25 | public void testObservable() { 26 | assertObservableProperties(RxBox::of); 27 | } 28 | 29 | @Test 30 | public void testMappedObservable() { 31 | Function> constructor = initial -> { 32 | Integer initialInt = Integer.parseInt(initial); 33 | RxBox box = RxBox.of(initialInt); 34 | return box.map(Ints.stringConverter().reverse()); 35 | }; 36 | assertObservableProperties(constructor); 37 | } 38 | 39 | static void assertObservableProperties(Function> constructor) { 40 | RxBox box = constructor.apply("1"); 41 | RxBox mappedBox = box.map(Ints.stringConverter()); 42 | 43 | RxAsserter asserter = RxAsserter.on(box); 44 | RxAsserter mapped = RxAsserter.on(mappedBox); 45 | asserter.assertValues("1"); 46 | mapped.assertValues(1); 47 | // double-setting doesn't transmit 48 | box.set("1"); 49 | asserter.assertValues("1"); 50 | mapped.assertValues(1); 51 | // but setting to a new value does 52 | box.set("2"); 53 | asserter.assertValues("1", "2"); 54 | mapped.assertValues(1, 2); 55 | // and setting back 56 | box.set("1"); 57 | asserter.assertValues("1", "2", "1"); 58 | mapped.assertValues(1, 2, 1); 59 | // and modify should work too 60 | box.modify(val -> val + "9"); 61 | asserter.assertValues("1", "2", "1", "19"); 62 | mapped.assertValues(1, 2, 1, 19); 63 | } 64 | 65 | @Test 66 | public void testEnforce() { 67 | { 68 | RxBox positiveBox = RxBox.of(1).enforce(Math::abs); 69 | RxAsserter asserter = RxAsserter.on(positiveBox); 70 | asserter.assertValues(1); 71 | positiveBox.set(-1); 72 | asserter.assertValues(1); 73 | positiveBox.set(2); 74 | asserter.assertValues(1, 2); 75 | positiveBox.set(-2); 76 | asserter.assertValues(1, 2); 77 | positiveBox.modify(i -> -i); 78 | asserter.assertValues(1, 2); 79 | } 80 | { 81 | RxBox positiveBox = RxBox.of(-1).enforce(Math::abs); 82 | RxAsserter asserter = RxAsserter.on(positiveBox); 83 | asserter.assertValues(1); 84 | positiveBox.set(-1); 85 | asserter.assertValues(1); 86 | positiveBox.set(-2); 87 | asserter.assertValues(1, 2); 88 | positiveBox.set(2); 89 | asserter.assertValues(1, 2); 90 | positiveBox.modify(i -> -i); 91 | asserter.assertValues(1, 2); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/RxExample.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2025 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | import java.awt.BorderLayout; 19 | import java.awt.Color; 20 | import java.awt.Graphics; 21 | import java.awt.Point; 22 | import java.awt.event.MouseAdapter; 23 | import java.awt.event.MouseEvent; 24 | import java.util.HashSet; 25 | import java.util.LinkedHashSet; 26 | import java.util.Optional; 27 | import java.util.Set; 28 | import javax.swing.JComponent; 29 | import javax.swing.JFrame; 30 | import javax.swing.JLabel; 31 | import kotlinx.coroutines.flow.FlowKt; 32 | 33 | @SuppressWarnings("serial") 34 | public class RxExample extends JFrame { 35 | 36 | public static void main(String[] args) { 37 | JFrame frame = new JFrame("DurianRx Example"); 38 | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 39 | frame.setLocationRelativeTo(null); 40 | frame.setLayout(new BorderLayout()); 41 | 42 | // show instructions 43 | JLabel instructions = new JLabel("Click and Ctrl+click to manipulate the selection"); 44 | frame.add(instructions, BorderLayout.NORTH); 45 | 46 | // show the Rx example 47 | frame.add(new RxGrid(), BorderLayout.CENTER); 48 | frame.setSize(CELL_SIZE * (NUM_CELLS + 1), CELL_SIZE * (NUM_CELLS + 2)); 49 | frame.setVisible(true); 50 | } 51 | 52 | private static final int NUM_CELLS = 5; 53 | private static final int CELL_SIZE = 50; 54 | 55 | static class RxGrid extends JComponent { 56 | /** The cell which the mouse is over. */ 57 | private RxGetter> rxMouseOver; 58 | /** The selected cells. */ 59 | private RxBox> rxSelection; 60 | 61 | RxGrid() { 62 | // maintain the position of the mouse 63 | RxBox mousePosition = RxBox.of(new Point(0, 0)); 64 | addMouseMotionListener(new MouseAdapter() { 65 | @Override 66 | public void mouseMoved(MouseEvent e) { 67 | mousePosition.set(e.getPoint()); 68 | } 69 | }); 70 | 71 | // maintain the position of the mouse in model terms 72 | rxMouseOver = mousePosition.map(p -> { 73 | int x = p.x / CELL_SIZE; 74 | int y = p.y / CELL_SIZE; 75 | if (x < NUM_CELLS && y < NUM_CELLS) { 76 | return Optional.of(x + y * NUM_CELLS); 77 | } else { 78 | return Optional.empty(); 79 | } 80 | }); 81 | 82 | // maintain the selection state 83 | rxSelection = RxBox.of(new HashSet<>()); 84 | addMouseListener(new MouseAdapter() { 85 | @Override 86 | public void mouseClicked(MouseEvent e) { 87 | rxMouseOver.get().ifPresent(cell -> { 88 | rxSelection.modify(set -> { 89 | HashSet selection = new LinkedHashSet<>(set); 90 | if (e.isControlDown()) { 91 | // control => toggle mouseOver item in selection 92 | if (selection.contains(cell)) { 93 | selection.remove(cell); 94 | } else { 95 | selection.add(cell); 96 | } 97 | } else { 98 | // no control => set selection to mouseOver 99 | selection.clear(); 100 | selection.add(cell); 101 | } 102 | return selection; 103 | }); 104 | }); 105 | } 106 | }); 107 | 108 | // trigger a repaint on any change 109 | Rx.subscribe(FlowKt.merge(rxMouseOver.asFlow(), rxSelection.asFlow()), anyChange -> { 110 | this.repaint(); 111 | }); 112 | } 113 | 114 | /** Paint a NUM_CELLS by NUM_CELLS grid. */ 115 | @Override 116 | public void paint(Graphics g) { 117 | int mouseOver = rxMouseOver.get().orElse(-1); 118 | Set selection = rxSelection.get(); 119 | for (int i = 0; i < NUM_CELLS * NUM_CELLS; ++i) { 120 | int x = i % NUM_CELLS; 121 | int y = i / NUM_CELLS; 122 | 123 | boolean isSelected = selection.contains(i); 124 | boolean isMouseOver = mouseOver == i; 125 | 126 | // set the background color based on the selection status 127 | Color color; 128 | if (isSelected) { 129 | color = isMouseOver ? Color.CYAN : Color.CYAN.darker(); 130 | } else { 131 | color = isMouseOver ? Color.WHITE : Color.WHITE.darker(); 132 | } 133 | // fill the rectangle and draw its border 134 | g.setColor(color); 135 | g.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE); 136 | g.setColor(Color.BLACK); 137 | g.drawRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE); 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/RxGetterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2022 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | import java.util.Optional; 23 | import org.junit.Assert; 24 | import org.junit.Test; 25 | 26 | public class RxGetterTest { 27 | @Test 28 | public void testMap() { 29 | RxBox> original = RxBox.of(Optional.empty()); 30 | RxGetter mapped = original.readOnly().map(Optional::isPresent); 31 | 32 | Asserter> assertOriginal = new Asserter<>(original); 33 | Asserter mappedOriginal = new Asserter<>(mapped); 34 | 35 | // we shoudl get the initial values 36 | assertOriginal.check(Optional.empty()); 37 | mappedOriginal.check(false); 38 | 39 | // nothing should happen because the value is equal to the previoius 40 | original.set(Optional.empty()); 41 | assertOriginal.check(); 42 | mappedOriginal.check(); 43 | 44 | // should see the "A" 45 | original.set(Optional.of("A")); 46 | assertOriginal.check(Optional.of("A")); 47 | mappedOriginal.check(true); 48 | 49 | // should see the "B", but the mapped shoudn't have a change 50 | original.set(Optional.of("B")); 51 | assertOriginal.check(Optional.of("B")); 52 | mappedOriginal.check(); 53 | 54 | // if we go back to empty, they should both see it 55 | original.set(Optional.empty()); 56 | assertOriginal.check(Optional.empty()); 57 | mappedOriginal.check(false); 58 | } 59 | 60 | private static class Asserter { 61 | private List newValues = new ArrayList<>(); 62 | 63 | public Asserter(RxGetter getter) { 64 | Rx.subscribe(getter, newValues::add); 65 | } 66 | 67 | @SafeVarargs 68 | public final void check(T... expected) { 69 | Assert.assertEquals(Arrays.asList(expected), newValues); 70 | newValues.clear(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/RxLockBoxTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.primitives.Ints; 20 | import org.junit.Assert; 21 | import org.junit.Test; 22 | 23 | public class RxLockBoxTest { 24 | static RxLockBox mappedConstructor(String initial) { 25 | Integer initialInt = Integer.parseInt(initial); 26 | RxLockBox box = RxLockBox.of(initialInt); 27 | return box.map(Ints.stringConverter().reverse()); 28 | } 29 | 30 | @Test 31 | public void testLockIdentity() { 32 | RxLockBox selfBox = RxLockBox.of(1); 33 | Assert.assertSame(selfBox, selfBox.lock()); 34 | 35 | Object otherLock = new Object(); 36 | RxLockBox otherBox = RxLockBox.of(1, otherLock); 37 | Assert.assertSame(otherLock, otherBox.lock()); 38 | } 39 | 40 | @Test 41 | public void testMappedLockIdentity() { 42 | RxLockBox intBox = RxLockBox.of(1); 43 | RxLockBox strBox = intBox.map(Ints.stringConverter().reverse()); 44 | Assert.assertSame(intBox.lock(), strBox.lock()); 45 | 46 | Object otherLock = new Object(); 47 | Assert.assertSame(otherLock, RxLockBox.of(1, otherLock).map(Ints.stringConverter().reverse()).lock()); 48 | } 49 | 50 | @Test 51 | public void testGetSetModify() { 52 | LockBoxTest.testLockingBehaviorGetSetModify("LockBox", RxLockBox::of); 53 | } 54 | 55 | @Test 56 | public void testMappedLock() { 57 | RxLockBox intBox = RxLockBox.of(1); 58 | RxLockBox strBox = intBox.map(Ints.stringConverter().reverse()); 59 | Assert.assertSame(intBox.lock(), strBox.lock()); 60 | } 61 | 62 | @Test 63 | public void testMappedGetSetModify() { 64 | LockBoxTest.testLockingBehaviorGetSetModify("LockBox mapped", RxLockBoxTest::mappedConstructor); 65 | } 66 | 67 | @Test 68 | public void testObservable() { 69 | RxBoxTest.assertObservableProperties(RxLockBox::of); 70 | } 71 | 72 | @Test 73 | public void testMappedObservable() { 74 | RxBoxTest.assertObservableProperties(RxLockBoxTest::mappedConstructor); 75 | } 76 | 77 | @Test 78 | public void testEnforcedProperties() { 79 | LockBoxTest.testLockingBehaviorGetSetModify("enforced", RxLockBoxTest::enforcedConstructor); 80 | LockBoxTest.testLockingBehaviorGetSetModify("mapThenEnforce", RxLockBoxTest::mapThenEnforceConstructor); 81 | LockBoxTest.testLockingBehaviorGetSetModify("enforceThenMap", RxLockBoxTest::enforceThenMapConstructor); 82 | 83 | RxBoxTest.assertObservableProperties(RxLockBoxTest::mappedConstructor); 84 | RxBoxTest.assertObservableProperties(RxLockBoxTest::mapThenEnforceConstructor); 85 | RxBoxTest.assertObservableProperties(RxLockBoxTest::enforceThenMapConstructor); 86 | } 87 | 88 | static RxLockBox enforcedConstructor(String initial) { 89 | RxLockBox strBox = RxLockBox.of(initial); 90 | return strBox.enforce(str -> Integer.toString(Math.abs(Integer.parseInt(str)))); 91 | } 92 | 93 | static RxLockBox mapThenEnforceConstructor(String initial) { 94 | Integer initialInt = Integer.parseInt(initial); 95 | RxLockBox intBox = RxLockBox.of(initialInt); 96 | RxLockBox strBox = intBox.map(Ints.stringConverter().reverse()); 97 | return strBox.enforce(str -> Integer.toString(Math.abs(Integer.parseInt(str)))); 98 | } 99 | 100 | static RxLockBox enforceThenMapConstructor(String initial) { 101 | Integer initialInt = Integer.parseInt(initial); 102 | RxLockBox box = RxLockBox.of(initialInt).enforce(Math::abs); 103 | return box.map(Ints.stringConverter().reverse()); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/common/rx/RxOrderedSetTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2022 DiffPlug 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.diffplug.common.rx; 17 | 18 | 19 | import com.diffplug.common.rx.RxOrderedSet.OnDuplicate; 20 | import java.util.ArrayList; 21 | import java.util.Arrays; 22 | import java.util.List; 23 | import org.junit.Assert; 24 | import org.junit.Test; 25 | 26 | public class RxOrderedSetTest { 27 | @Test 28 | public void testDisallowDuplicates() { 29 | // first and last 30 | testCase(Arrays.asList(1, 2, 3, 4, 5), Arrays.asList(1, 2, 3, 4, 5, 1), OnDuplicate.TAKE_FIRST, Arrays.asList(1, 2, 3, 4, 5)); 31 | testCase(Arrays.asList(1, 2, 3, 4, 5), Arrays.asList(1, 2, 3, 4, 5, 1), OnDuplicate.TAKE_LAST, Arrays.asList(2, 3, 4, 5, 1)); 32 | } 33 | 34 | @Test(expected = Exception.class) 35 | public void testDisallowDuplicatesError() { 36 | testCase(Arrays.asList(), Arrays.asList(1, 1), OnDuplicate.ERROR, Arrays.asList()); 37 | } 38 | 39 | private void testCase(List before, List after, OnDuplicate policy, List expected) { 40 | // create the initial list with its policy 41 | RxOrderedSet list = RxOrderedSet.of(new ArrayList<>(before), policy); 42 | // set the new value 43 | list.set(new ArrayList<>(after)); 44 | // test the resulting value 45 | Assert.assertEquals(expected, list.get()); 46 | } 47 | } 48 | --------------------------------------------------------------------------------