/* * Copyright (C) 2010 Google Inc. * * 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 com.google.api.explorer.client.base; import com.google.api.explorer.client.base.ApiDirectory.ServiceDefinition; import com.google.api.explorer.client.base.ApiService.CallStyle; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.gwt.core.client.Callback; import com.google.gwt.user.client.rpc.AsyncCallback; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; /** * Utility class to encapsulate logic of loading services. * * @author jasonhall@google.com (Jason Hall) */ public class ServiceLoader { /** * Delegate format for an observer of service and directory loaded events. */ public interface ServiceLoaderDelegate { /** * Invoked when a service has been loaded. * * @param service Service definition for the service which has been loaded. */ public void serviceLoaded(ApiService service); /** * Invoked when a directory document has been loaded and parsed. * * @param directoryServices Parsed set of services from the directory document. */ public void directoryLoaded(Set<ServiceDefinition> directoryServices); } private final ApiServiceFactory googleApi; /** * Delegate property which can be set to be notified of events. Default value discards * notifications. */ public ServiceLoaderDelegate delegate = new ServiceLoaderDelegate() { @Override public void serviceLoaded(ApiService service) { // Intentionally blank, null implementation. } @Override public void directoryLoaded(Set<ServiceDefinition> directoryServices) { // Intentionally blank, null implementation. } }; /** * List of services that should not be returned by directory because they would provide a bad * experience in APIs explorer. */ private static final Set<String> SERVICE_NAME_BLACKLIST = Collections.emptySet(); /** * List of specific versions of services that should not be returned by directory because they * would provide a bad experience. */ private static final Set<String> SERVICE_ID_BLACKLIST = ImmutableSet.of("drive:v1"); @VisibleForTesting final Map<String, ApiService> cache = Maps.newHashMap(); final Multimap<String, Callback<ApiService, String>> outstandingRequestCallbacks = HashMultimap.create(); private Set<ServiceDefinition> directoryCache; /** * Create an instance. * * @param googleApi Factory from which to obtain services on the wire. */ public ServiceLoader(ApiServiceFactory googleApi) { this.googleApi = googleApi; } /** * Load the specified service from cache or request it from the discovery service. * * @param name Name of the service. * @param version Version of the service. * @param callback Callback to invoke when loading is complete. */ public void loadService(String name, String version, Callback<ApiService, String> callback) { final String cacheKey = generateCacheKey(name, version, CallStyle.REST); // Handle the request immediately if possible. if (cache.containsKey(cacheKey)) { callback.onSuccess(cache.get(cacheKey)); return; } outstandingRequestCallbacks.put(cacheKey, callback); // Only send the request if our request is the only one waiting on the resource. if (outstandingRequestCallbacks.get(cacheKey).size() == 1) { googleApi.createService(name, version, CallStyle.REST, new AsyncCallback<ApiService>() { @Override public void onSuccess(ApiService service) { cache.put(cacheKey, service); for (Callback<ApiService, String> cb : copyAndClearOutstandingCallbacks(cacheKey)) { cb.onSuccess(service); } delegate.serviceLoaded(service); } @Override public void onFailure(Throwable caught) { String failureMessage = caught.getMessage(); for (Callback<ApiService, String> cb : copyAndClearOutstandingCallbacks(cacheKey)) { cb.onFailure(failureMessage); } } }); } } /** * Copy the callbacks associated with the specified cache key and remove them from the list of * outstanding callbacks. */ private Collection<Callback<ApiService, String>> copyAndClearOutstandingCallbacks( String cacheKey) { Collection<Callback<ApiService, String>> callbacks = ImmutableList.copyOf(outstandingRequestCallbacks.get(cacheKey)); outstandingRequestCallbacks.removeAll(cacheKey); return callbacks; } /** * Alternate interface for callers to use when they don't care about when the service has been * loaded (e.g. search). */ public void backgroundLoadService(String serviceId) { String[] components = serviceId.split(":"); Preconditions.checkArgument(components.length == 2); String serviceName = components[0]; String version = components[1]; loadService(serviceName, version, new Callback<ApiService, String>() { @Override public void onFailure(String reason) { // Intentionally blank. } @Override public void onSuccess(ApiService result) { // Intentionally blank. } }); } /** * Load the directory document from either cache or the wire and notify the specified callback * when done. */ public void loadServiceDefinitions(final Callback<Set<ServiceDefinition>, String> callback) { if (directoryCache == null) { googleApi.loadApiDirectory(new AsyncCallback<Set<ServiceDefinition>>() { @Override public void onSuccess(Set<ServiceDefinition> unfiltered) { // Filter the list of services according to the blacklist. directoryCache = Sets.filter(unfiltered, new Predicate<ServiceDefinition>() { @Override public boolean apply(ServiceDefinition service) { return !SERVICE_NAME_BLACKLIST.contains(service.getName()) && !SERVICE_ID_BLACKLIST.contains(service.getId()); } }); callback.onSuccess(directoryCache); delegate.directoryLoaded(directoryCache); } @Override public void onFailure(Throwable caught) { callback.onFailure(caught.getMessage()); } }); } else { callback.onSuccess(directoryCache); } } /** * Load the directory document in the background. */ public void backgroundLoadServiceDefinitions() { loadServiceDefinitions(new Callback<Set<ServiceDefinition>, String>() { @Override public void onSuccess(Set<ServiceDefinition> directoryServices) { // Intentionally blank. } @Override public void onFailure(String reason) { // Intentionally blank. } }); } /** * Create a cache key that encodes the service name, version name, and call * style. Example: urlshortener_v1_REST */ @VisibleForTesting static String generateCacheKey( String serviceName, String versionName, CallStyle callStyle) { if (serviceName == null || versionName == null || callStyle == null) { return null; } List<String> portions = ImmutableList.of(serviceName, versionName, callStyle.name()); return Joiner.on("_").join(portions); } }