1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.databinding.tool.processing;
18
19import android.databinding.tool.processing.scopes.FileScopeProvider;
20import android.databinding.tool.processing.scopes.LocationScopeProvider;
21import android.databinding.tool.processing.scopes.ScopeProvider;
22import android.databinding.tool.store.Location;
23import android.databinding.tool.util.Preconditions;
24
25import java.util.ArrayList;
26import java.util.Arrays;
27import java.util.Collections;
28import java.util.HashSet;
29import java.util.List;
30
31/**
32 * Utility class to keep track of "logical" stack traces, which we can use to print better error
33 * reports.
34 */
35public class Scope {
36
37    private static ThreadLocal<ScopeEntry> sScopeItems = new ThreadLocal<ScopeEntry>();
38    static List<ScopedException> sDeferredExceptions = new ArrayList<ScopedException>();
39
40    public static void enter(final Location location) {
41        enter(new LocationScopeProvider() {
42            @Override
43            public List<Location> provideScopeLocation() {
44                return Collections.singletonList(location);
45            }
46        });
47    }
48    public static void enter(ScopeProvider scopeProvider) {
49        ScopeEntry peek = sScopeItems.get();
50        ScopeEntry entry = new ScopeEntry(scopeProvider, peek);
51        sScopeItems.set(entry);
52    }
53
54    public static void exit() {
55        ScopeEntry entry = sScopeItems.get();
56        Preconditions.checkNotNull(entry, "Inconsistent scope exit");
57        sScopeItems.set(entry.mParent);
58    }
59
60    public static void defer(ScopedException exception) {
61        sDeferredExceptions.add(exception);
62    }
63
64    private static void registerErrorInternal(String msg, int scopeIndex,
65            ScopeProvider... scopeProviders) {
66        if (scopeProviders == null || scopeProviders.length <= scopeIndex) {
67            defer(new ScopedException(msg));
68        } else if (scopeProviders[scopeIndex] == null) {
69            registerErrorInternal(msg, scopeIndex + 1, scopeProviders);
70        } else {
71            try {
72                Scope.enter(scopeProviders[scopeIndex]);
73                registerErrorInternal(msg, scopeIndex + 1, scopeProviders);
74            } finally {
75                Scope.exit();
76            }
77        }
78    }
79
80    /**
81     * Convenience method to add an error in a known list of scopes, w/o adding try catch flows.
82     * <p>
83     * This code actually starts entering the given scopes 1 by 1, starting from 0. When list is
84     * consumed, it creates the exception and defers if possible then exits from the provided
85     * scopes.
86     * <p>
87     * Note that these scopes are added on top of the already existing scopes.
88     *
89     * @param msg The exception message
90     * @param scopeProviders The list of additional scope providers to enter. Null scopes are
91     *                       automatically ignored.
92     */
93    public static void registerError(String msg, ScopeProvider... scopeProviders) {
94        registerErrorInternal(msg, 0, scopeProviders);
95    }
96
97    public static void assertNoError() {
98        if (sDeferredExceptions.isEmpty()) {
99            return;
100        }
101        StringBuilder sb = new StringBuilder();
102        HashSet<String> messages = new HashSet<String>();
103        for (ScopedException ex : sDeferredExceptions) {
104            final String message = ex.getMessage();
105            if (!messages.contains(message)) {
106                sb.append(message).append("\n");
107                messages.add(message);
108            }
109        }
110        throw new RuntimeException("Found data binding errors.\n" + sb.toString());
111    }
112
113    static String produceScopeLog() {
114        StringBuilder sb = new StringBuilder();
115        sb.append("full scope log\n");
116        ScopeEntry top = sScopeItems.get();
117        while (top != null) {
118            ScopeProvider provider = top.mProvider;
119            sb.append("---").append(provider).append("\n");
120            if (provider instanceof FileScopeProvider) {
121                sb.append("file:").append(((FileScopeProvider) provider).provideScopeFilePath())
122                        .append("\n");
123            }
124            if (provider instanceof LocationScopeProvider) {
125                LocationScopeProvider loc = (LocationScopeProvider) provider;
126                sb.append("loc:");
127                List<Location> locations = loc.provideScopeLocation();
128                if (locations == null) {
129                    sb.append("null\n");
130                } else {
131                    for (Location location : locations) {
132                        sb.append(location).append("\n");
133                    }
134                }
135            }
136            top = top.mParent;
137        }
138        sb.append("---\n");
139        return sb.toString();
140    }
141
142    static ScopedErrorReport createReport() {
143        ScopeEntry top = sScopeItems.get();
144        String filePath = null;
145        List<Location> locations = null;
146        while (top != null && (filePath == null || locations == null)) {
147            ScopeProvider provider = top.mProvider;
148            if (locations == null && provider instanceof LocationScopeProvider) {
149                locations = findAbsoluteLocationFrom(top, (LocationScopeProvider) provider);
150            }
151            if (filePath == null && provider instanceof FileScopeProvider) {
152                filePath = ((FileScopeProvider) provider).provideScopeFilePath();
153            }
154            top = top.mParent;
155        }
156        return new ScopedErrorReport(filePath, locations);
157    }
158
159    private static List<Location> findAbsoluteLocationFrom(ScopeEntry entry,
160            LocationScopeProvider top) {
161        List<Location> locations = top.provideScopeLocation();
162        if (locations == null || locations.isEmpty()) {
163            return null;
164        }
165        if (locations.size() == 1) {
166            return Collections.singletonList(locations.get(0).toAbsoluteLocation());
167        }
168        // We have more than 1 location. Depending on the scope, we may or may not want all of them
169        List<Location> chosen = new ArrayList<Location>();
170        for (Location location : locations) {
171            Location absLocation = location.toAbsoluteLocation();
172            if (validatedContained(entry.mParent, absLocation)) {
173                chosen.add(absLocation);
174            }
175        }
176        return chosen.isEmpty() ? locations : chosen;
177    }
178
179    private static boolean validatedContained(ScopeEntry parent, Location absLocation) {
180        if (parent == null) {
181            return true;
182        }
183        ScopeProvider provider = parent.mProvider;
184        if (!(provider instanceof LocationScopeProvider)) {
185            return validatedContained(parent.mParent, absLocation);
186        }
187        List<Location> absoluteParents = findAbsoluteLocationFrom(parent,
188                (LocationScopeProvider) provider);
189        if (absoluteParents != null) {
190            for (Location location : absoluteParents) {
191                if (location.contains(absLocation)) {
192                    return true;
193                }
194            }
195        }
196        return false;
197    }
198
199    private static class ScopeEntry {
200
201        ScopeProvider mProvider;
202
203        ScopeEntry mParent;
204
205        public ScopeEntry(ScopeProvider scopeProvider, ScopeEntry parent) {
206            mProvider = scopeProvider;
207            mParent = parent;
208        }
209    }
210}