Java tutorial
/* * Copyright (C) 2015 The Android Open Source Project * * 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.android.tools.idea.structure.services; import com.android.tools.idea.ui.properties.AbstractProperty; import com.android.tools.idea.ui.properties.InvalidationListener; import com.android.tools.idea.ui.properties.ObservableValue; import com.android.tools.idea.ui.properties.core.BoolValueProperty; import com.android.tools.idea.ui.properties.core.ObservableBool; import com.google.common.base.Splitter; import com.google.common.collect.Maps; import com.intellij.openapi.util.EmptyRunnable; import org.jetbrains.annotations.NotNull; import java.util.Iterator; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.Callable; /** * A generic mapping of strings to {@link ObservableValue}s and {@link Runnable} actions. Adding * entries here allows them to be referenced within service.xml. * <p/> * Some of the values added to a context are marked as "watched", using * {@link #putWatchedValue(String, AbstractProperty)}. Those values are special, and modifying * any of them will mark this context as modified. {@link #snapshot()} and {@link #restore()} will * also save and revert watched values - this is useful, for example, if a user is modifying a * service but then cancels their changes. * <p/> * As a convention, you should organize your keys into namespaces, using periods to delimit them. * For example, instead of "countOfAnalyticsProjects" and "countOfAdsProjects", prefer instead * "analytics.projects.count" and "ads.projects.count" * <p/> * TODO: Revisit this class so that the whole concept of snapshot and restore is unecessary. Every * time we show the UI of a service, we should instead create and initialize a ServiceContext from * scratch? */ public final class ServiceContext { private final Map<String, ObservableValue> myValues = Maps.newHashMap(); private final Map<String, Runnable> myActions = Maps.newHashMap(); private final Map<AbstractProperty, Object> myWatched = new WeakHashMap<AbstractProperty, Object>(); private final BoolValueProperty myInstalled = new BoolValueProperty(); private final BoolValueProperty myModified = new BoolValueProperty(); private final BoolValueProperty myHiddenFromStructureDialog = new BoolValueProperty(); private final InvalidationListener myWatchedListener = new InvalidationListener() { @Override public void onInvalidated(@NotNull ObservableValue<?> sender) { myModified.set(true); } }; @NotNull private final String myBuildSystemId; private Runnable myBeforeShown = EmptyRunnable.INSTANCE; private Callable<Boolean> myTestValidity = new Callable<Boolean>() { @Override public Boolean call() throws Exception { return true; } }; public ServiceContext(@NotNull String buildSystemId) { myBuildSystemId = buildSystemId; } @NotNull String getBuildSystemId() { return myBuildSystemId; } /** * Set a callback to call before this service's UI is shown to the user. This is a useful place * to put in any expensive operations, like network requests, that should only happen when * needed. */ public void setBeforeShownCallback(@NotNull Runnable beforeShown) { myBeforeShown = beforeShown; } /** * Set a callback to call when the user wishes to install this service, to ensure the values * are OK. */ public void setIsValidCallback(@NotNull Callable<Boolean> testValidity) { myTestValidity = testValidity; } public void beginEditing() { myBeforeShown.run(); if (myWatched.isEmpty()) { myModified.set(isValid()); } } public void finishEditing() { if (!myModified.get()) { return; } myModified.set(isValid()); } public void cancelEditing() { myModified.set(false); } /** * A property which indicates whether this service is already installed into the current module * or not. */ public BoolValueProperty installed() { return myInstalled; } /** * A property which indicates if any of the watched values have been changed. * * @see #putWatchedValue(String, AbstractProperty) */ public ObservableBool modified() { return myModified; } /** * TODO: This is a temporary measure as we update the Project Structure dialog and consider * better integrating the PSD and the Assistant toolbar. Either we need to reconcile the two * features (e.g. PSD delegates responsibility to the Assistant toolbar), or if we find out * there are valid cases for a service to appear in one and not the other, at least we should * architect the code more cleanly than by branching on a boolean property like this. */ public BoolValueProperty hiddenFromStructureDialog() { return myHiddenFromStructureDialog; } /** * Take a snapshot of the current state of all watched values and clear the modified flag. You * can later {@link #restore()} the values to the snapshot. * * @see #putWatchedValue(String, AbstractProperty) */ public void snapshot() { // TODO: The snapshot concept was added when we thought users would be able to modify the // values of installed services. However, that's currently not supported and may never be. // Remove this method? (Related: http://b.android.com/178452) for (AbstractProperty property : myWatched.keySet()) { myWatched.put(property, property.get()); } myModified.set(false); } /** * Restore the values captured by {@link #snapshot()} */ public void restore() { for (AbstractProperty property : myWatched.keySet()) { //noinspection unchecked property.set(myWatched.get(property)); } myModified.set(false); } /** * Put a named value into the context. */ public void putValue(@NotNull String key, @NotNull ObservableValue value) { myValues.put(key, value); } /** * Put a named value into the context which can be {@link #snapshot()}ed and {@link #restore()}d. * Watched values are also used to determine whether this service has been {@link #modified()}. */ public void putWatchedValue(@NotNull String key, @NotNull AbstractProperty property) { putValue(key, property); property.addWeakListener(myWatchedListener); myWatched.put(property, property.get()); } /** * Put a named {@link Runnable} into the context. */ public void putAction(@NotNull String key, @NotNull Runnable action) { myActions.put(key, action); } @NotNull public ObservableValue getValue(@NotNull String key) { ObservableValue value = myValues.get(key); if (value == null) { throw new IllegalArgumentException(String.format("Service context: Value \"%1$s\" not found.", key)); } return value; } @NotNull public Runnable getAction(@NotNull String key) { Runnable action = myActions.get(key); if (action == null) { throw new IllegalArgumentException(String.format("Service context: Action \"%1$s\" not found.", key)); } return action; } /** * Converts this service context, which is itself backed by a flat map, into a hierarchical map, * a data structure that freemarker works well with. * <p/> * For example, a service context with the values "parent.child1" and "parent.child2" will return * a map that is nested like so * <pre> * parent * child1 * child2 * </pre> */ @NotNull public Map<String, Object> toValueMap() { Map<String, Object> valueMap = Maps.newHashMap(); Splitter splitter = Splitter.on('.'); for (String key : myValues.keySet()) { ObservableValue value = getValue(key); Map<String, Object> currLevel = valueMap; Iterator<String> keyParts = splitter.split(key).iterator(); while (keyParts.hasNext()) { String keyPart = keyParts.next(); if (keyParts.hasNext()) { if (currLevel.containsKey(keyPart)) { currLevel = (Map<String, Object>) currLevel.get(keyPart); } else { Map<String, Object> nextLevel = Maps.newHashMap(); currLevel.put(keyPart, nextLevel); currLevel = nextLevel; } } else { // We're the last part of the key currLevel.put(keyPart, value); } } } return valueMap; } /** * Check if this service is valid - if any of the user's values are bad, we shouldn't install * this service. */ private boolean isValid() { try { return myTestValidity.call(); } catch (Exception e) { return false; } } }