View Javadoc
1   /**
2    * This file Copyright (c) 2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.framework;
35  
36  
37  import java.io.Serializable;
38  import java.util.Objects;
39  import java.util.Optional;
40  
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  
44  import io.reactivex.BackpressureStrategy;
45  import io.reactivex.disposables.Disposable;
46  import io.reactivex.functions.Consumer;
47  import io.reactivex.subjects.BehaviorSubject;
48  import io.reactivex.subjects.Subject;
49  
50  /**
51   * Observable property. Provides two flavours of observation:
52   * <ul>
53   * <li>null-safe, which yields optionals to the observers</li>
54   * <li>nullable, which may broadcast nulls</li>
55   * </ul>
56   * <p>
57   * Also provides the mutation capabilities in imperative and functional styles. Imperative is
58   * useful when we merely want to swap one value with another, whereas the functional one can be
59   * used when e.g. another element needs to be added to a collection.
60   * </p>
61   * @param <T> item type.
62   */
63  public interface ContextProperty<T> extends Serializable {
64  
65      Logger log = LoggerFactory.getLogger(ContextProperty.class);
66  
67      Disposable observeNullable(Consumer<T> action);
68  
69      Disposable observe(Consumer<Optional<T>> action);
70  
71      Optional<T> value();
72  
73      default void set(T value) {
74          set(value, false);
75      }
76  
77      void set(T value, boolean shouldNotifyOnSameItem);
78  
79      void mutate(Consumer<T> mutator);
80  
81      default T nullableValue() {
82          //TODO: Not get if that's nullable!
83          return value().get();
84      }
85  
86      /**
87       * A wrapper around {@link ContextProperty}.
88       * @param <T> item type.
89       */
90      class Wrapper<T> implements ContextProperty<T> {
91  
92          private final ContextProperty<T> delegate;
93  
94          public Wrapper(ContextProperty<T> delegate) {
95              this.delegate = delegate;
96          }
97  
98          @Override
99          public Disposable observeNullable(Consumer<T> action) {
100             return this.delegate.observeNullable(action);
101         }
102 
103         @Override
104         public Disposable observe(Consumer<Optional<T>> action) {
105             return this.delegate.observe(action);
106         }
107 
108         @Override
109         public Optional<T> value() {
110             return this.delegate.value();
111         }
112 
113         @Override
114         public void set(T value, boolean shouldNotifyOnSameItem) {
115             this.delegate.set(value, shouldNotifyOnSameItem);
116         }
117 
118         @Override
119         public void mutate(Consumer<T> mutator) {
120             this.delegate.mutate(mutator);
121         }
122     }
123 
124     //    Flowable<Optional<T>> observe();
125 
126     /**
127      * Default implementation of {@link ContextProperty}.
128      *
129      * @param <T>
130      *     property value type
131      */
132     class Impl<T> implements ContextProperty<T> {
133 
134         private static final Logger log = LoggerFactory.getLogger(Impl.class);
135 
136         private T lastValue = null;
137 
138         private T pendingValueUpdate = null;
139 
140         private Subject<Optional<T>> subject = BehaviorSubject.createDefault(Optional.empty());
141 
142         private boolean updatesMuted;
143 
144 
145         Impl() {
146             subject.onNext(Optional.empty());
147         }
148 
149         public Disposable observeNullable(Consumer<T> action) {
150             return subject
151                     .toFlowable(BackpressureStrategy.LATEST)
152                     .map(optional -> optional)
153                     .subscribe(
154                             optional -> {
155                                 muteUpdates();
156                                 try {
157                                     action.accept(optional.orElse(null));
158                                 } finally {
159                                     unmuteUpdates();
160                                 }
161                             },
162                             e -> log.error("Failed to dispatch context property change: {}", e.getMessage(), e));
163         }
164 
165         @Override
166         public Disposable observe(Consumer<Optional<T>> action) {
167             return subject.subscribe(value -> {
168                 muteUpdates();
169                 try {
170                     action.accept(value);
171                 } finally {
172                     unmuteUpdates();
173                 }
174             }, e -> log.error("Failed to dispatch context property change: {}", e.getMessage(), e));
175 
176         }
177 
178         @Override
179         public void mutate(Consumer<T> mutator) {
180             value().ifPresent(value -> {
181                 try {
182                     mutator.accept(value);
183                 } catch (Exception e) {
184                     log.error("{}", e.getMessage(), e);
185                 }
186                 doSet(value, true);
187             });
188         }
189 
190         @Override
191         public Optional<T> value() {
192             return Optional.ofNullable(this.lastValue);
193         }
194 
195         @Override
196         public void set(T value, boolean shouldNotifyOnSameItem) {
197             doSet(value, shouldNotifyOnSameItem);
198         }
199 
200         public void doSet(T value, boolean shouldNotifyOnSameItem) {
201             if (updatesMuted) {
202                 pendingValueUpdate = value;
203                 return;
204             }
205 
206             if (!shouldNotifyOnSameItem && Objects.equals(value, this.lastValue)) {
207                 return;
208             }
209 
210             this.lastValue = value;
211             muteUpdates();
212             try {
213                 this.subject.onNext(Optional.ofNullable(value));
214             } finally {
215                unmuteUpdates();
216             }
217         }
218 
219         private void muteUpdates() {
220             this.updatesMuted = true;
221         }
222 
223         private void unmuteUpdates() {
224             if (pendingValueUpdate != null) {
225                 doSet(pendingValueUpdate, false);
226                 pendingValueUpdate = null;
227             }
228 
229             this.updatesMuted = false;
230         }
231     }
232 }