/* * Copyright(c) 2017 lizhaotailang * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.github.marktony.espresso.data.source; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import io.github.marktony.espresso.data.Package; import io.reactivex.Observable; import io.reactivex.ObservableSource; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; /** * Created by lizhaotailang on 2017/2/12. * Concrete implementation to load packages from the data sources into a cache. * <p/> * For simplicity, this implements a dumb synchronisation between locally persisted data and data * obtained from the server, by using the remote data source only if the local database * is not the latest. */ public class PackagesRepository implements PackagesDataSource { @Nullable private static PackagesRepository INSTANCE = null; @NonNull private final PackagesDataSource packagesRemoteDataSource; @NonNull private final PackagesDataSource packagesLocalDataSource; private Map<String, Package> cachedPackages; // Prevent direct instantiation private PackagesRepository(@NonNull PackagesDataSource packagesRemoteDataSource, @NonNull PackagesDataSource packagesLocalDataSource) { this.packagesRemoteDataSource = packagesRemoteDataSource; this.packagesLocalDataSource = packagesLocalDataSource; } // The access for other classes. public static PackagesRepository getInstance(@NonNull PackagesDataSource packagesRemoteDataSource, @NonNull PackagesDataSource packagesLocalDataSource) { if (INSTANCE == null) { INSTANCE = new PackagesRepository(packagesRemoteDataSource, packagesLocalDataSource); } return INSTANCE; } public static void destroyInstance() { INSTANCE = null; } /** * It is designed to gotten the packages from both database * and network. Which are faster then return them. * But in our app, we need not to update the data by accessing the network * when user enter the app every time because we run a service in backyard. * So we just return the data from database. * But in future, it may change. * @return Packages from {@link io.github.marktony.espresso.data.source.local.PackagesLocalDataSource}. */ @Override public Observable<List<Package>> getPackages() { if (cachedPackages != null) { return Observable.fromCallable(new Callable<List<Package>>() { @Override public List<Package> call() throws Exception { List<Package> arrayList = new ArrayList<Package>(cachedPackages.values()); // Sort by the timestamp to make the list shown in a descend way Collections.sort(arrayList, new Comparator<Package>() { @Override public int compare(Package o1, Package o2) { if (o1.getTimestamp() > o2.getTimestamp()) { return -1; } else if (o1.getTimestamp() < o2.getTimestamp()) { return 1; } return 0; } }); return arrayList; } }); } else { cachedPackages = new LinkedHashMap<>(); // Return the cached packages. return packagesLocalDataSource .getPackages() .flatMap(new Function<List<Package>, ObservableSource<List<Package>>>() { @Override public ObservableSource<List<Package>> apply(List<Package> packages) throws Exception { return Observable .fromIterable(packages) .doOnNext(new Consumer<Package>() { @Override public void accept(Package aPackage) throws Exception { cachedPackages.put(aPackage.getNumber(), aPackage); } }) .toList() .toObservable(); } }); } } /** * Get a package of specific number from data source. * @param packNumber The primary key or the package id. See {@link Package}. * @return The package. */ @Override public Observable<Package> getPackage(@NonNull final String packNumber) { Package cachedPackage = getPackageWithNumber(packNumber); if (cachedPackage != null) { return Observable.just(cachedPackage); } return getPackageWithNumberFromLocalRepository(packNumber); } /** * Save the package to data source and cache. * It is supposed to save it to database and network too. * But we have no cloud(The account system) yet. * It may change either. * @param pack The package to save. See more @{@link Package}. */ @Override public void savePackage(@NonNull Package pack) { packagesLocalDataSource.savePackage(pack); if (cachedPackages == null) { cachedPackages = new LinkedHashMap<>(); } if (!isPackageExist(pack.getNumber())) { cachedPackages.put(pack.getNumber(), pack); } } /** * Delete a package from data source and cache. * @param packageId The primary id or in another words, the package number. * See more @{@link Package#number}. */ @Override public void deletePackage(@NonNull String packageId) { packagesLocalDataSource.deletePackage(packageId); cachedPackages.remove(packageId); } /** * Refresh the packages. * Just call the remote data source and it will make everything done. * @return The observable packages. */ @Override public Observable<List<Package>> refreshPackages() { return packagesRemoteDataSource .refreshPackages() .flatMap(new Function<List<Package>, ObservableSource<List<Package>>>() { @Override public ObservableSource<List<Package>> apply(List<Package> packages) throws Exception { return Observable .fromIterable(packages) .doOnNext(new Consumer<Package>() { @Override public void accept(Package aPackage) throws Exception { Package p = cachedPackages.get(aPackage.getNumber()); if (p != null) { p.setData(aPackage.getData()); p.setPushable(true); p.setReadable(true); } } }) .toList() .toObservable(); } }); } /** * Refresh one package. * Just call the remote data source and it will make everything done. * @param packageId The primary key(The package number). * See more @{@link Package#number}. * @return The observable package. */ @Override public Observable<Package> refreshPackage(@NonNull final String packageId) { return packagesRemoteDataSource .refreshPackage(packageId) .flatMap(new Function<Package, ObservableSource<Package>>() { @Override public ObservableSource<Package> apply(Package p) throws Exception { return Observable .just(p) .doOnNext(new Consumer<Package>() { @Override public void accept(Package aPackage) throws Exception { Package pkg = cachedPackages.get(aPackage.getNumber()); if (pkg != null) { pkg.setData(aPackage.getData()); pkg.setReadable(true); } } }); } }); } /** * Set the packages read in both data source and cache. */ @Override public void setAllPackagesRead() { packagesLocalDataSource.setAllPackagesRead(); if (cachedPackages == null) { cachedPackages = new HashMap<>(); } for (Package p : cachedPackages.values()) { p.setReadable(false); p.setPushable(false); } } /** * Set a package with specific number read or unread new * in both data source and cache. * @param packageId The primary key (The package number). * See more @{@link Package#number}. * @param readable Unread new or read. */ @Override public void setPackageReadable(@NonNull String packageId, boolean readable) { Package p = cachedPackages.get(packageId); p.setReadable(readable); p.setPushable(readable); packagesLocalDataSource.setPackageReadable(packageId, readable); } /** * Checkout out the existence of a package with specific number. * @param packageId The primary key or in another words, the package number. * See more @{@link Package#number}. * @return Whether the package exists. */ @Override public boolean isPackageExist(@NonNull String packageId) { return getPackageWithNumber(packageId) != null; } @Override public void updatePackageName(@NonNull String packageId, @NonNull String name) { if (getPackageWithNumber(packageId) != null) { getPackageWithNumber(packageId).setName(name); } packagesLocalDataSource.updatePackageName(packageId, name); } @Override public Observable<List<Package>> searchPackages(@NonNull String keyWords) { // Do nothing but just let local data source handle it. return packagesLocalDataSource.searchPackages(keyWords); } /** * Get a package with package number. * @param packNumber The package id(number). See more @{@link Package#number}. * @return The package with specific number. */ @Nullable private Package getPackageWithNumber(@NonNull String packNumber) { if (cachedPackages == null || cachedPackages.isEmpty()) { return null; } else { return cachedPackages.get(packNumber); } } /** * As the function name says, get a package from local repository. * @param packNumber The package number. * @return The package. */ @Nullable private Observable<Package> getPackageWithNumberFromLocalRepository(@NonNull final String packNumber) { return packagesLocalDataSource .getPackage(packNumber) .doOnNext(new Consumer<Package>() { @Override public void accept(Package aPackage) throws Exception { if (cachedPackages == null) { cachedPackages = new LinkedHashMap<>(); } cachedPackages.put(packNumber, aPackage); } }); } }