1 /* 2 * Copyright (C) 2011 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 17 package com.android.tools.lint; 18 19 import static com.android.tools.lint.detector.api.LintConstants.DOT_9PNG; 20 import static com.android.tools.lint.detector.api.LintConstants.DOT_PNG; 21 import static com.android.tools.lint.detector.api.LintUtils.endsWith; 22 23 import com.google.common.annotations.Beta; 24 import com.google.common.io.ByteStreams; 25 import com.google.common.io.Closeables; 26 import com.google.common.io.Files; 27 28 import java.io.File; 29 import java.io.FileOutputStream; 30 import java.io.IOException; 31 import java.io.InputStream; 32 import java.io.UnsupportedEncodingException; 33 import java.net.URL; 34 import java.net.URLEncoder; 35 import java.util.HashMap; 36 import java.util.List; 37 import java.util.Map; 38 39 /** A reporter is an output generator for lint warnings 40 * <p> 41 * <b>NOTE: This is not a public or final API; if you rely on this be prepared 42 * to adjust your code for the next tools release.</b> 43 */ 44 @Beta 45 public abstract class Reporter { 46 protected final Main mClient; 47 protected final File mOutput; 48 protected String mTitle = "Lint Report"; 49 protected boolean mSimpleFormat; 50 protected boolean mBundleResources; 51 protected Map<String, String> mUrlMap; 52 protected File mResources; 53 protected Map<File, String> mResourceUrl = new HashMap<File, String>(); 54 protected Map<String, File> mNameToFile = new HashMap<String, File>(); 55 56 /** 57 * Write the given warnings into the report 58 * 59 * @param errorCount the number of errors 60 * @param warningCount the number of warnings 61 * @param issues the issues to be reported 62 * @throws IOException if an error occurs 63 */ write(int errorCount, int warningCount, List<Warning> issues)64 public abstract void write(int errorCount, int warningCount, List<Warning> issues) 65 throws IOException; 66 Reporter(Main client, File output)67 protected Reporter(Main client, File output) { 68 mClient = client; 69 mOutput = output; 70 } 71 72 /** 73 * Sets the report title 74 * 75 * @param title the title of the report 76 */ setTitle(String title)77 public void setTitle(String title) { 78 mTitle = title; 79 } 80 81 /** @return the title of the report */ getTitle()82 public String getTitle() { 83 return mTitle; 84 } 85 86 /** 87 * Sets whether the report should bundle up resources along with the HTML report. 88 * This implies a non-simple format (see {@link #setSimpleFormat(boolean)}). 89 * 90 * @param bundleResources if true, copy images into a directory relative to 91 * the report 92 */ setBundleResources(boolean bundleResources)93 public void setBundleResources(boolean bundleResources) { 94 mBundleResources = bundleResources; 95 mSimpleFormat = false; 96 } 97 98 /** 99 * Sets whether the report should use simple formatting (meaning no JavaScript, 100 * embedded images, etc). 101 * 102 * @param simpleFormat whether the formatting should be simple 103 */ setSimpleFormat(boolean simpleFormat)104 public void setSimpleFormat(boolean simpleFormat) { 105 mSimpleFormat = simpleFormat; 106 } 107 108 /** 109 * Returns whether the report should use simple formatting (meaning no JavaScript, 110 * embedded images, etc). 111 * 112 * @return whether the report should use simple formatting 113 */ isSimpleFormat()114 public boolean isSimpleFormat() { 115 return mSimpleFormat; 116 } 117 118 getUrl(File file)119 String getUrl(File file) { 120 if (mBundleResources && !mSimpleFormat) { 121 String url = getRelativeResourceUrl(file); 122 if (url != null) { 123 return url; 124 } 125 } 126 127 if (mUrlMap != null) { 128 String path = file.getAbsolutePath(); 129 try { 130 // Perform the comparison using URLs such that we properly escape spaces etc. 131 String pathUrl = URLEncoder.encode(path, "UTF-8"); //$NON-NLS-1$ 132 for (Map.Entry<String, String> entry : mUrlMap.entrySet()) { 133 String prefix = entry.getKey(); 134 String prefixUrl = URLEncoder.encode(prefix, "UTF-8"); //$NON-NLS-1$ 135 if (pathUrl.startsWith(prefixUrl)) { 136 String relative = pathUrl.substring(prefixUrl.length()); 137 return entry.getValue() 138 + relative.replace("%2F", "/"); //$NON-NLS-1$ //$NON-NLS-2$ 139 } 140 } 141 } catch (UnsupportedEncodingException e) { 142 // This shouldn't happen for UTF-8 143 System.err.println("Invalid URL map specification - " + e.getLocalizedMessage()); 144 } 145 } 146 147 return null; 148 } 149 150 /** Encodes the given String as a safe URL substring, escaping spaces etc */ encodeUrl(String url)151 static String encodeUrl(String url) { 152 try { 153 return URLEncoder.encode(url, "UTF-8"); //$NON-NLS-1$ 154 } catch (UnsupportedEncodingException e) { 155 // This shouldn't happen for UTF-8 156 System.err.println("Invalid string " + e.getLocalizedMessage()); 157 return url; 158 } 159 } 160 161 /** Set mapping of path prefixes to corresponding URLs in the HTML report */ setUrlMap(Map<String, String> urlMap)162 void setUrlMap(Map<String, String> urlMap) { 163 mUrlMap = urlMap; 164 } 165 166 /** Gets a pointer to the local resource directory, if any */ getResourceDir()167 File getResourceDir() { 168 if (mResources == null && mBundleResources) { 169 mResources = computeResourceDir(); 170 if (mResources == null) { 171 mBundleResources = false; 172 } 173 } 174 175 return mResources; 176 } 177 178 /** Finds/creates the local resource directory, if possible */ computeResourceDir()179 File computeResourceDir() { 180 String fileName = mOutput.getName(); 181 int dot = fileName.indexOf('.'); 182 if (dot != -1) { 183 fileName = fileName.substring(0, dot); 184 } 185 186 File resources = new File(mOutput.getParentFile(), fileName + "_files"); //$NON-NLS-1$ 187 if (!resources.exists() && !resources.mkdir()) { 188 resources = null; 189 } 190 191 return resources; 192 } 193 194 /** Returns a URL to a local copy of the given file, or null */ getRelativeResourceUrl(File file)195 protected String getRelativeResourceUrl(File file) { 196 String resource = mResourceUrl.get(file); 197 if (resource != null) { 198 return resource; 199 } 200 201 String name = file.getName(); 202 if (!endsWith(name, DOT_PNG) || endsWith(name, DOT_9PNG)) { 203 return null; 204 } 205 206 // Attempt to make local copy 207 File resourceDir = getResourceDir(); 208 if (resourceDir != null) { 209 String base = file.getName(); 210 211 File path = mNameToFile.get(base); 212 if (path != null && !path.equals(file)) { 213 // That filename already exists and is associated with a different path: 214 // make a new unique version 215 for (int i = 0; i < 100; i++) { 216 base = '_' + base; 217 path = mNameToFile.get(base); 218 if (path == null || path.equals(file)) { 219 break; 220 } 221 } 222 } 223 224 File target = new File(resourceDir, base); 225 try { 226 Files.copy(file, target); 227 } catch (IOException e) { 228 return null; 229 } 230 return resourceDir.getName() + '/' + encodeUrl(base); 231 } 232 return null; 233 } 234 235 /** Returns a URL to a local copy of the given resource, or null. There is 236 * no filename conflict resolution. */ addLocalResources(URL url)237 protected String addLocalResources(URL url) { 238 // Attempt to make local copy 239 File resourceDir = computeResourceDir(); 240 if (resourceDir != null) { 241 String base = url.getFile(); 242 base = base.substring(base.lastIndexOf('/') + 1); 243 mNameToFile.put(base, new File(url.toExternalForm())); 244 245 File target = new File(resourceDir, base); 246 try { 247 FileOutputStream output = new FileOutputStream(target); 248 InputStream input = url.openStream(); 249 ByteStreams.copy(input, output); 250 Closeables.closeQuietly(output); 251 Closeables.closeQuietly(input); 252 } catch (IOException e) { 253 return null; 254 } 255 return resourceDir.getName() + '/' + encodeUrl(base); 256 } 257 return null; 258 } 259 }