/* * 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 android.databinding.tool.processing; import android.databinding.tool.processing.scopes.FileScopeProvider; import android.databinding.tool.processing.scopes.LocationScopeProvider; import android.databinding.tool.processing.scopes.ScopeProvider; import android.databinding.tool.store.Location; import android.databinding.tool.util.Preconditions; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; /** * Utility class to keep track of "logical" stack traces, which we can use to print better error * reports. */ public class Scope { private static ThreadLocal sScopeItems = new ThreadLocal(); static List sDeferredExceptions = new ArrayList(); public static void enter(final Location location) { enter(new LocationScopeProvider() { @Override public List provideScopeLocation() { return Arrays.asList(location); } }); } public static void enter(ScopeProvider scopeProvider) { ScopeEntry peek = sScopeItems.get(); ScopeEntry entry = new ScopeEntry(scopeProvider, peek); sScopeItems.set(entry); } public static void exit() { ScopeEntry entry = sScopeItems.get(); Preconditions.checkNotNull(entry, "Inconsistent scope exit"); sScopeItems.set(entry.mParent); } public static void defer(ScopedException exception) { sDeferredExceptions.add(exception); } private static void registerErrorInternal(String msg, int scopeIndex, ScopeProvider... scopeProviders) { if (scopeProviders == null || scopeProviders.length <= scopeIndex) { defer(new ScopedException(msg)); } else if (scopeProviders[scopeIndex] == null) { registerErrorInternal(msg, scopeIndex + 1, scopeProviders); } else { try { Scope.enter(scopeProviders[scopeIndex]); registerErrorInternal(msg, scopeIndex + 1, scopeProviders); } finally { Scope.exit(); } } } /** * Convenience method to add an error in a known list of scopes, w/o adding try catch flows. *

* This code actually starts entering the given scopes 1 by 1, starting from 0. When list is * consumed, it creates the exception and defers if possible then exits from the provided * scopes. *

* Note that these scopes are added on top of the already existing scopes. * * @param msg The exception message * @param scopeProviders The list of additional scope providers to enter. Null scopes are * automatically ignored. */ public static void registerError(String msg, ScopeProvider... scopeProviders) { registerErrorInternal(msg, 0, scopeProviders); } public static void assertNoError() { if (sDeferredExceptions.isEmpty()) { return; } StringBuilder sb = new StringBuilder(); HashSet messages = new HashSet(); for (ScopedException ex : sDeferredExceptions) { final String message = ex.getMessage(); if (!messages.contains(message)) { sb.append(message).append("\n"); messages.add(message); } } throw new RuntimeException("Found data binding errors.\n" + sb.toString()); } static String produceScopeLog() { StringBuilder sb = new StringBuilder(); sb.append("full scope log\n"); ScopeEntry top = sScopeItems.get(); while (top != null) { ScopeProvider provider = top.mProvider; sb.append("---").append(provider).append("\n"); if (provider instanceof FileScopeProvider) { sb.append("file:").append(((FileScopeProvider) provider).provideScopeFilePath()) .append("\n"); } if (provider instanceof LocationScopeProvider) { LocationScopeProvider loc = (LocationScopeProvider) provider; sb.append("loc:"); List locations = loc.provideScopeLocation(); if (locations == null) { sb.append("null\n"); } else { for (Location location : locations) { sb.append(location).append("\n"); } } } top = top.mParent; } sb.append("---\n"); return sb.toString(); } static ScopedErrorReport createReport() { ScopeEntry top = sScopeItems.get(); String filePath = null; List locations = null; while (top != null && (filePath == null || locations == null)) { ScopeProvider provider = top.mProvider; if (locations == null && provider instanceof LocationScopeProvider) { locations = findAbsoluteLocationFrom(top, (LocationScopeProvider) provider); } if (filePath == null && provider instanceof FileScopeProvider) { filePath = ((FileScopeProvider) provider).provideScopeFilePath(); } top = top.mParent; } return new ScopedErrorReport(filePath, locations); } private static List findAbsoluteLocationFrom(ScopeEntry entry, LocationScopeProvider top) { List locations = top.provideScopeLocation(); if (locations == null || locations.isEmpty()) { return null; } if (locations.size() == 1) { return Arrays.asList(locations.get(0).toAbsoluteLocation()); } // We have more than 1 location. Depending on the scope, we may or may not want all of them List chosen = new ArrayList(); for (Location location : locations) { Location absLocation = location.toAbsoluteLocation(); if (validatedContained(entry.mParent, absLocation)) { chosen.add(absLocation); } } return chosen.isEmpty() ? locations : chosen; } private static boolean validatedContained(ScopeEntry parent, Location absLocation) { if (parent == null) { return true; } ScopeProvider provider = parent.mProvider; if (!(provider instanceof LocationScopeProvider)) { return validatedContained(parent.mParent, absLocation); } List absoluteParents = findAbsoluteLocationFrom(parent, (LocationScopeProvider) provider); for (Location location : absoluteParents) { if (location.contains(absLocation)) { return true; } } return false; } private static class ScopeEntry { ScopeProvider mProvider; ScopeEntry mParent; public ScopeEntry(ScopeProvider scopeProvider, ScopeEntry parent) { mProvider = scopeProvider; mParent = parent; } } }