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
17package com.android.tools.lint;
18
19import com.android.tools.lint.checks.BuiltinIssueRegistry;
20import com.android.tools.lint.detector.api.Issue;
21import com.android.tools.lint.detector.api.Location;
22import com.android.tools.lint.detector.api.Position;
23import com.google.common.annotations.Beta;
24import com.google.common.base.Charsets;
25import com.google.common.io.Files;
26
27import java.io.BufferedWriter;
28import java.io.File;
29import java.io.IOException;
30import java.io.Writer;
31import java.util.List;
32
33/**
34 * A reporter which emits lint results into an XML report.
35 * <p>
36 * <b>NOTE: This is not a public or final API; if you rely on this be prepared
37 * to adjust your code for the next tools release.</b>
38 */
39@Beta
40public class XmlReporter extends Reporter {
41    private final Writer mWriter;
42
43    /**
44     * Constructs a new {@link XmlReporter}
45     *
46     * @param client the client
47     * @param output the output file
48     * @throws IOException if an error occurs
49     */
50    public XmlReporter(Main client, File output) throws IOException {
51        super(client, output);
52        mWriter = new BufferedWriter(Files.newWriter(output, Charsets.UTF_8));
53    }
54
55    @Override
56    public void write(int errorCount, int warningCount, List<Warning> issues) throws IOException {
57        mWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");      //$NON-NLS-1$
58        mWriter.write("<issues format=\"3\"");                              //$NON-NLS-1$
59        String revision = mClient.getRevision();
60        if (revision != null) {
61            mWriter.write(String.format(" by=\"lint %1$s\"", revision));    //$NON-NLS-1$
62        }
63        mWriter.write(">\n");                                               //$NON-NLS-1$
64
65        if (issues.size() > 0) {
66            for (Warning warning : issues) {
67                mWriter.write('\n');
68                indent(mWriter, 1);
69                mWriter.write("<issue"); //$NON-NLS-1$
70                Issue issue = warning.issue;
71                writeAttribute(mWriter, 2, "id", issue.getId());                      //$NON-NLS-1$
72                writeAttribute(mWriter, 2, "severity",
73                        warning.severity.getDescription());
74                writeAttribute(mWriter, 2, "message", warning.message);               //$NON-NLS-1$
75
76                writeAttribute(mWriter, 2, "category",                                //$NON-NLS-1$
77                        issue.getCategory().getFullName());
78                writeAttribute(mWriter, 2, "priority",                                //$NON-NLS-1$
79                        Integer.toString(issue.getPriority()));
80                writeAttribute(mWriter, 2, "summary", issue.getDescription());        //$NON-NLS-1$
81                writeAttribute(mWriter, 2, "explanation", issue.getExplanation());    //$NON-NLS-1$
82                if (issue.getMoreInfo() != null) {
83                    writeAttribute(mWriter, 2, "url", issue.getMoreInfo());           //$NON-NLS-1$
84                }
85                if (warning.errorLine != null && !warning.errorLine.isEmpty()) {
86                    String line = warning.errorLine;
87                    int index1 = line.indexOf('\n');
88                    if (index1 != -1) {
89                        int index2 = line.indexOf('\n', index1 + 1);
90                        if (index2 != -1) {
91                            String line1 = line.substring(0, index1);
92                            String line2 = line.substring(index1 + 1, index2);
93                            writeAttribute(mWriter, 2, "errorLine1", line1);          //$NON-NLS-1$
94                            writeAttribute(mWriter, 2, "errorLine2", line2);       //$NON-NLS-1$
95                        }
96                    }
97                }
98                if (mClient.getRegistry() instanceof BuiltinIssueRegistry &&
99                        ((BuiltinIssueRegistry) mClient.getRegistry()).hasAutoFix(
100                                "adt", issue)) { //$NON-NLS-1$
101                    writeAttribute(mWriter, 2, "quickfix", "adt");      //$NON-NLS-1$ //$NON-NLS-2$
102                }
103
104                assert (warning.file != null) == (warning.location != null);
105
106                if (warning.file != null) {
107                    assert warning.location.getFile() == warning.file;
108                }
109
110                Location location = warning.location;
111                if (location != null) {
112                    mWriter.write(">\n"); //$NON-NLS-1$
113                    while (location != null) {
114                        indent(mWriter, 2);
115                        mWriter.write("<location"); //$NON-NLS-1$
116                        String path = mClient.getDisplayPath(warning.project, location.getFile());
117                        writeAttribute(mWriter, 3, "file", path);  //$NON-NLS-1$
118                        Position start = location.getStart();
119                        if (start != null) {
120                            int line = start.getLine();
121                            int column = start.getColumn();
122                            if (line >= 0) {
123                                // +1: Line numbers internally are 0-based, report should be
124                                // 1-based.
125                                writeAttribute(mWriter, 3, "line",         //$NON-NLS-1$
126                                        Integer.toString(line + 1));
127                                if (column >= 0) {
128                                    writeAttribute(mWriter, 3, "column",   //$NON-NLS-1$
129                                            Integer.toString(column + 1));
130                                }
131                            }
132                        }
133
134                        mWriter.write("/>\n"); //$NON-NLS-1$
135                        location = location.getSecondary();
136                    }
137                    indent(mWriter, 1);
138                    mWriter.write("</issue>\n"); //$NON-NLS-1$
139                } else {
140                    mWriter.write('\n');
141                    indent(mWriter, 1);
142                    mWriter.write("/>\n");  //$NON-NLS-1$
143                }
144            }
145        }
146
147        mWriter.write("\n</issues>\n");       //$NON-NLS-1$
148        mWriter.close();
149
150        String path = mOutput.getAbsolutePath();
151        System.out.println(String.format("Wrote XML report to %1$s", path));
152    }
153
154    private static void writeAttribute(Writer writer, int indent, String name, String value)
155            throws IOException {
156        writer.write('\n');
157        indent(writer, indent);
158        writer.write(name);
159        writer.write('=');
160        writer.write('"');
161        for (int i = 0, n = value.length(); i < n; i++) {
162            char c = value.charAt(i);
163            switch (c) {
164                case '"':
165                    writer.write("&quot;"); //$NON-NLS-1$
166                    break;
167                case '\'':
168                    writer.write("&apos;"); //$NON-NLS-1$
169                    break;
170                case '&':
171                    writer.write("&amp;");  //$NON-NLS-1$
172                    break;
173                case '<':
174                    writer.write("&lt;");   //$NON-NLS-1$
175                    break;
176                default:
177                    writer.write(c);
178                    break;
179            }
180        }
181        writer.write('"');
182    }
183
184    private static void indent(Writer writer, int indent) throws IOException {
185        for (int level = 0; level < indent; level++) {
186            writer.write("    "); //$NON-NLS-1$
187        }
188    }
189}