001/*-
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.server.provider;
021
022import jakarta.annotation.Nonnull;
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026import java.util.ArrayList;
027import java.util.Collections;
028import java.util.HashSet;
029import java.util.LinkedHashSet;
030import java.util.List;
031import java.util.Set;
032import java.util.function.Supplier;
033
034/**
035 * This is a generic implementation of the <a href="https://refactoring.guru/design-patterns/observer">Observer Design Pattern</a>.
036 * We use this to pass sets of beans from exporting Spring application contexts to importing Spring application contexts. We defer
037 * resolving the observed beans via a Supplier to give the exporting context a chance to initialize the beans before they are used.
038 * @param <T> the class of the Observer
039 * <p>
040 * A typical usage pattern would be:
041 * <ol>
042 *           <li>Create {@link ObservableSupplierSet} in exporter context.</li>
043 *           <li>Add all the suppliers in the exporter context.</li>
044 *           <li>Attach the importer to the {@link ObservableSupplierSet}</li>
045 *           <li>Importer calls {@link ObservableSupplierSet#getSupplierResults} and processes all the beans</li>
046 *           <li>Some other service beans may add more suppliers later as a part of their initialization and the observer handlers will process them accordingly</li>
047 *           <li>Those other service beans should call {@link ObservableSupplierSet#removeSupplier(Supplier)} in a @PreDestroy method so they are properly cleaned up if those services are shut down or restarted</li>
048 * </ol>
049 *
050 */
051public class ObservableSupplierSet<T extends IObservableSupplierSetObserver> {
052        private static final Logger ourLog = LoggerFactory.getLogger(ObservableSupplierSet.class);
053
054        private final Set<T> myObservers = Collections.synchronizedSet(new HashSet<>());
055
056        private final Set<Supplier<Object>> mySuppliers = new LinkedHashSet<>();
057
058        /** Add a supplier and notify all observers
059         *
060         * @param theSupplier supplies the object to be observed
061         */
062        public void addSupplier(@Nonnull Supplier<Object> theSupplier) {
063                if (mySuppliers.add(theSupplier)) {
064                        myObservers.forEach(observer -> observer.update(theSupplier));
065                }
066        }
067
068        /** Remove a supplier and notify all observers.  CAUTION, you might think that this code would work, but it does not:
069         * <code>
070         * observableSupplierSet.addSupplier(() -> myBean);
071         * ...
072         * observableSupplierSet.removeSupplier(() -> myBean);
073         * </code>
074         * the removeSupplier in this example would fail because it is a different lambda instance from the first.  Instead,
075         * you need to store the supplier between the add and remove:
076         * <code>
077         * mySupplier = () -> myBean;
078         * observableSupplierSet.addSupplier(mySupplier);
079         * ...
080         * observableSupplierSet.removeSupplier(mySupplier);
081         * </code>
082         *
083         * @param theSupplier the supplier to be removed
084         */
085        public void removeSupplier(@Nonnull Supplier<Object> theSupplier) {
086                if (mySuppliers.remove(theSupplier)) {
087                        myObservers.forEach(observer -> observer.remove(theSupplier));
088                } else {
089                        ourLog.warn("Failed to remove supplier", new RuntimeException());
090                }
091        }
092
093        /**
094         * Attach an observer to this observableSupplierSet.  This observer will be notified every time a supplier is added or removed.
095         * @param theObserver the observer to be notified
096         */
097        public void attach(T theObserver) {
098                myObservers.add(theObserver);
099        }
100
101        /**
102         * Detach an observer from this observableSupplierSet, so it is no longer notified when suppliers are added and removed.
103         * @param theObserver the observer to be removed
104         */
105        public void detach(T theObserver) {
106                myObservers.remove(theObserver);
107        }
108
109        /**
110         *
111         * @return a list of get() being called on all suppliers.
112         */
113        protected List<Object> getSupplierResults() {
114                List<Object> retVal = new ArrayList<>();
115                for (Supplier<Object> next : mySuppliers) {
116                        Object nextRp = next.get();
117                        if (nextRp != null) {
118                                retVal.add(nextRp);
119                        }
120                }
121                return retVal;
122        }
123}