/* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.datasource; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import java.util.List; import com.facebook.common.executors.CallerThreadExecutor; import com.facebook.common.internal.Objects; import com.facebook.common.internal.Preconditions; import com.facebook.common.internal.Supplier; /** * {@link DataSource} supplier that provides a data source which will forward results of the first * available data source. * * <p>Data sources are obtained in order. Only if the current data source fails, or if it finishes * without result, the next one will be tried. */ @ThreadSafe public class FirstAvailableDataSourceSupplier<T> implements Supplier<DataSource<T>> { private final List<Supplier<DataSource<T>>> mDataSourceSuppliers; private FirstAvailableDataSourceSupplier(List<Supplier<DataSource<T>>> dataSourceSuppliers) { Preconditions.checkArgument(!dataSourceSuppliers.isEmpty(), "List of suppliers is empty!"); mDataSourceSuppliers = dataSourceSuppliers; } public static <T> FirstAvailableDataSourceSupplier<T> create( List<Supplier<DataSource<T>>> dataSourceSuppliers) { return new FirstAvailableDataSourceSupplier<T>(dataSourceSuppliers); } @Override public DataSource<T> get() { return new FirstAvailableDataSource(); } @Override public int hashCode() { return mDataSourceSuppliers.hashCode(); } @Override public boolean equals(Object other) { if (other == this) { return true; } if (!(other instanceof FirstAvailableDataSourceSupplier)) { return false; } FirstAvailableDataSourceSupplier that = (FirstAvailableDataSourceSupplier) other; return Objects.equal(this.mDataSourceSuppliers, that.mDataSourceSuppliers); } @Override public String toString() { return Objects.toStringHelper(this) .add("list", mDataSourceSuppliers) .toString(); } @ThreadSafe private class FirstAvailableDataSource extends AbstractDataSource<T> { private int mIndex = 0; private DataSource<T> mCurrentDataSource = null; private DataSource<T> mDataSourceWithResult = null; public FirstAvailableDataSource() { if (!startNextDataSource()) { setFailure(new RuntimeException("No data source supplier or supplier returned null.")); } } @Override @Nullable public synchronized T getResult() { DataSource<T> dataSourceWithResult = getDataSourceWithResult(); return (dataSourceWithResult != null) ? dataSourceWithResult.getResult() : null; } @Override public synchronized boolean hasResult() { DataSource<T> dataSourceWithResult = getDataSourceWithResult(); return (dataSourceWithResult != null) && dataSourceWithResult.hasResult(); } @Override public boolean close() { DataSource<T> currentDataSource; DataSource<T> dataSourceWithResult; synchronized (FirstAvailableDataSource.this) { // it's fine to call {@code super.close()} within a synchronized block because we don't // implement {@link #closeResult()}, but perform result closing ourselves. if (!super.close()) { return false; } currentDataSource = mCurrentDataSource; mCurrentDataSource = null; dataSourceWithResult = mDataSourceWithResult; mDataSourceWithResult = null; } closeSafely(dataSourceWithResult); closeSafely(currentDataSource); return true; } private boolean startNextDataSource() { Supplier<DataSource<T>> dataSourceSupplier = getNextSupplier(); DataSource<T> dataSource = (dataSourceSupplier != null) ? dataSourceSupplier.get() : null; if (setCurrentDataSource(dataSource) && dataSource != null) { dataSource.subscribe(new InternalDataSubscriber(), CallerThreadExecutor.getInstance()); return true; } else { closeSafely(dataSource); return false; } } @Nullable private synchronized Supplier<DataSource<T>> getNextSupplier() { if (!isClosed() && mIndex < mDataSourceSuppliers.size()) { return mDataSourceSuppliers.get(mIndex++); } return null; } private synchronized boolean setCurrentDataSource(DataSource<T> dataSource) { if (isClosed()) { return false; } mCurrentDataSource = dataSource; return true; } private synchronized boolean clearCurrentDataSource(DataSource<T> dataSource) { if (isClosed() || dataSource != mCurrentDataSource) { return false; } mCurrentDataSource = null; return true; } @Nullable private synchronized DataSource<T> getDataSourceWithResult() { return mDataSourceWithResult; } private void maybeSetDataSourceWithResult( DataSource<T> dataSource, boolean isFinished) { DataSource<T> oldDataSource = null; synchronized (FirstAvailableDataSource.this) { if (dataSource != mCurrentDataSource || dataSource == mDataSourceWithResult) { return; } // If we didn't have any result so far, we got one now, so we'll set // {@code mDataSourceWithResult} to point to the current data source. // If we did have a result which came from another data source, // we'll only set {@code mDataSourceWithResult} to point to the current // data source if it has finished (i.e. the new result is final). if (mDataSourceWithResult == null || isFinished) { oldDataSource = mDataSourceWithResult; mDataSourceWithResult = dataSource; } } closeSafely(oldDataSource); } private void onDataSourceFailed(DataSource<T> dataSource) { if (!clearCurrentDataSource(dataSource)) { return; } if (dataSource != getDataSourceWithResult()) { closeSafely(dataSource); } if (!startNextDataSource()) { setFailure(dataSource.getFailureCause()); } } private void onDataSourceNewResult(DataSource<T> dataSource) { maybeSetDataSourceWithResult(dataSource, dataSource.isFinished()); // If the data source with the new result is our {@code mDataSourceWithResult}, // we have to notify our subscribers about the new result. if (dataSource == getDataSourceWithResult()) { setResult(null, dataSource.isFinished()); } } private void closeSafely(DataSource<T> dataSource) { if (dataSource != null) { dataSource.close(); } } private class InternalDataSubscriber implements DataSubscriber<T> { @Override public void onFailure(DataSource<T> dataSource) { FirstAvailableDataSource.this.onDataSourceFailed(dataSource); } @Override public void onCancellation(DataSource<T> dataSource) { } @Override public void onNewResult(DataSource<T> dataSource) { if (dataSource.hasResult()) { FirstAvailableDataSource.this.onDataSourceNewResult(dataSource); } else if (dataSource.isFinished()) { FirstAvailableDataSource.this.onDataSourceFailed(dataSource); } } } } }