/* Copyright (c) 2008 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.gdata.util; import com.google.common.annotations.VisibleForTesting; import com.google.gdata.client.Service; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * The VersionRegistry class is used to manage and retrieve version information * about executing services. The registry supports the ability to configure * versions for a running thread (via the {@link #setThreadVersion(Version)} * method) or global defaults that will apply to all threads (using the * {@link #addDefaultVersion(Version, boolean)} method. Thread defaults will * have precedence over global defaults if present for the same service. * * The class provides a singleton instance that is being used to manage version * information. This instance is initialized by the {@link #ensureRegistry()} * method. The active VersionRegistry instance can be retrieved using the * {@link #get()} method. This method will throw an * {@link IllegalStateException} if the version registry has not been * initialized to aid in the detection of when version-conditional code is being * executed in an environment where versions have net been configured. * * The {@link VersionRegistry#getVersion(Class)} method can be used to request * the version information for a particular service. * * A model for writing version conditional code based upon the registry is: * <code> * Version myServiceVersion = * VersionRegistry.get().getVersion(MyService.class); * if (myServiceVersion.isCompatible(MyService.VERSIONS.V1) { * ... execute V1-specific handling ... * } * </code> * * VersionRegistry access is thread-safe. */ public class VersionRegistry { /** * Singleton registry instance. The singleton is lazily initialized when the * {@link #ensureRegistry()} method is called. The reason for this design is * to support the detect of version-conditional code running in unit tests. * Such tests need to be run in a version-aware test environment (that will * validate the behavior against all valid versions), so having a model so * that they will fail by default is helpful to guarantee this. */ private static VersionRegistry versionRegistry; /** * Maintains the per-thread version information. The field may be * {@code null} if thread tracking is not enabled and the thread local value * may be {@code null} if no versions have been set for the current thread. */ private ThreadLocal<List<Version>> threadVersions = new ThreadLocal<List<Version>>(); /** * Maintains the global defaults. */ private List<Version> defaultVersions = new ArrayList<Version>(); /** * Returns the current VersionRegistry, creating it if necessary. The * {@link #get()} method is preferred for most registry usage, as it enables * the discovery of the execution of version-conditional code in an * environment (such as unit test cases) where versioning has not been * properly configured. */ public static synchronized VersionRegistry ensureRegistry() { if (versionRegistry == null) { versionRegistry = new VersionRegistry(); } return versionRegistry; } /** * Resets the VersionRegistry instance to {@code null}. This means that any * subsequent attempts to run version-specific code without version * configuration will result in an {@link IllegalStateException} in * {@link #get()}. */ @VisibleForTesting static void reset() { versionRegistry = null; } /** * Returns the version registry being used to manage version information. * @return the active version registry instance. * @throws IllegalStateException if the registry has not been initialized. */ public static final VersionRegistry get() { if (versionRegistry == null) { // This should never happen for client, server, or code running in a // unit test context. Missing version information indicates that the // version registry has not been properly initialized to meet the // expectations of version-dependent code. In the case of test // execution, this generally means the test should be annotated to // indicate a version dependency (see the TestVersion annotation) and // also should be run in the context of a VersionedTestSuite that // ensures all supported versions are tested. throw new IllegalStateException("Uninitialized version registry"); } return versionRegistry; } /** * Constructs a new Version instance based upon the value of a Java system * property associated with a {@link Service} class. The system property name * is computed from the service class name with ".version" appended. The * syntax of the property value is {@code "[service]<major>[.<minor>]"}. The * default value of the service is assumed to be the initiating or target * service and the minor revision will be assumed to be zero if not present. * If the associated system property is not set, the method will return * {@code null}. * * @param serviceClass service class to use in computing the version property * name. * @return the {@link Version} computed from the property of {@code null} if * the property is not set. * @throws IllegalStateException if the property value does not contain valid * revision information. */ public static Version getVersionFromProperty( Class<? extends Service> serviceClass) { String propertyName = serviceClass.getName() + ".version"; String versionProperty = System.getProperty(propertyName); if (versionProperty == null) { return null; } try { return new Version(serviceClass, versionProperty); } catch (IllegalArgumentException iae) { throw new IllegalStateException( "Invalid version property value: " + propertyName, iae); } } /** * Takes a list of {@link Version} instances and merges it into another * list. A version in the source list will overwrite any value for the * same service (if any) in the target list. * @param target the target list of versions to merge into. * @param source the source list of versions that will be merged. */ @VisibleForTesting static void mergeVersions(List<Version> target, List<Version> source) { // Check for conflicts with target list before making any changes, // accumulating the list of changed versions. for (Version checkVersion : source) { Version currentVersion = Version.findServiceVersion(target, checkVersion.getServiceClass()); if (currentVersion != null) { target.remove(currentVersion); } } // Add all of the new versions. target.addAll(source); } /** * Takes a {@link Version} instance and merges it into another * list, validating that any duplicate information for a given service * is a compatible version. * @param target the target list of versions to merge into. * @param source the source version that will be merged. */ @VisibleForTesting static void mergeVersions(List<Version> target, Version source) { mergeVersions(target, Arrays.asList(new Version [] { source })); } /** * Returns the list of default versions for the registry. The default version * is the version that will be used if no version is explicitly selected. * * @return list of default versions. */ public List<Version> getDefaultVersions() { return defaultVersions; } /** * Adds a default version to the version registry. This will overwrite any * existing default version for the same service. * * @param newDefault default version to add to the registry * (not <code>null</code>) * @param includeImplied if {@code true}, indicates that all implied versions * associated with the new default should be set as defaults too. */ public void addDefaultVersion(Version newDefault, boolean includeImplied) { // Implement the addition using a copy into a new array. This is done to // avoid requiring full synchronization of access to defaultVersions, where // additions will be infrequent and often happen at initialization time. ArrayList<Version> newDefaults = new ArrayList<Version>(defaultVersions); if (includeImplied) { mergeVersions(newDefaults, newDefault.getImpliedVersions()); } else { mergeVersions(newDefaults, newDefault); } // Replace the current defaults with the updated list. defaultVersions = Collections.unmodifiableList(newDefaults); } /** * Sets the desired version for the current thread to the provided values. * This method will update any existing request version information set by * defaults or a previous call to this method. The specified version (and * any related implied versions} will be set for the current thread until the * {@link #resetThreadVersion()} method is called to reset to the version * information back to the default state. * * @param version the new active version for this request. */ public void setThreadVersion(Version version) { // Set the thread local to the list of versions implied by the requested // version. threadVersions.set( Collections.unmodifiableList(version.getImpliedVersions())); } /** * Returns the list of versions associated with the current thread or * {@code null} if there are currently no thread versions. * * @return thread version list or {@code null} */ public List<Version> getThreadVersions() { return threadVersions.get(); } /** * Resets the version information for the current thread back to the * default state. */ public void resetThreadVersion() { if (threadVersions != null) { threadVersions.remove(); } } /** * Returns the the current list of active versions. This list takes both * global defaults and thread versions into account. */ @VisibleForTesting List<Version> getVersions() { List<Version> defaultList = getDefaultVersions(); List<Version> threadList = getThreadVersions(); if (threadList == null) { return defaultList; } List<Version> combinedList = new ArrayList<Version>(defaultList.size() + threadList.size()); combinedList.addAll(defaultList); mergeVersions(combinedList, threadList); return combinedList; } /** * Returns the version of a service. * * @param serviceClass of the service to return. * @return version of the service. * @throws IllegalStateException if no version information could be found for * the requested service. */ public Version getVersion(Class<? extends Service> serviceClass) { Version v = null; List<Version> threadList = getThreadVersions(); if (threadList != null) { v = Version.findServiceVersion(threadList, serviceClass); } if (v == null) { v = Version.findServiceVersion(getDefaultVersions(), serviceClass); if (v == null) { // This should never happen for client, server, or code running in a // unit test context. Missing version information indicates that the // version registry has not been properly initialized to meet the // expectations of version-dependent code. In the case of test // execution, this generally means the test should be annotated to // indicate a version dependency (see the TestVersion annotation) and // also should be run in the context of a VersionedTestSuite that // ensures all supported versions are tested. throw new IllegalStateException( "Attempt to access version information for unversioned service:" + serviceClass); } } return v; } /** * Resets the VersionRegistry to a clean state with no thread local * configuration and the specified set of version defaults. * * @param initialDefaults the list of default versions that should be used to * initialize the version registry, or {@code null} for an empty * default list. */ @VisibleForTesting public synchronized void reset(List<Version> initialDefaults) { threadVersions = new ThreadLocal<List<Version>>(); if (initialDefaults != null) { defaultVersions = new ArrayList<Version>(initialDefaults); } else { defaultVersions = new ArrayList<Version>(); } } }