001/*
002 * Copyright (C) 2015-2021 KeepSafe Software
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package com.getkeepsafe.dexcount.report;
017
018import com.getkeepsafe.dexcount.CountReporter;
019import com.getkeepsafe.dexcount.PackageTree;
020import com.getkeepsafe.dexcount.colors.Color;
021import com.getkeepsafe.dexcount.colors.IOConsumer;
022import com.getkeepsafe.dexcount.colors.Styleable;
023import com.getkeepsafe.dexcount.thrift.TreeGenOutput;
024import com.microsoft.thrifty.protocol.CompactProtocol;
025import com.microsoft.thrifty.protocol.Protocol;
026import com.microsoft.thrifty.transport.Transport;
027import okio.BufferedSource;
028import okio.GzipSource;
029import okio.Okio;
030import org.gradle.api.logging.LogLevel;
031import org.gradle.workers.WorkAction;
032import org.jetbrains.annotations.NotNull;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036import java.io.File;
037import java.io.IOException;
038import java.io.PrintWriter;
039import java.io.Writer;
040
041public abstract class ReportOutputWorker implements WorkAction<ReportOutputWorkerParams> {
042    private static final Logger LOGGER = LoggerFactory.getLogger(ReportOutputWorker.class);
043
044    @Override
045    public void execute() {
046        try {
047            actuallyExecute();
048        } catch (IOException e) {
049            LOGGER.error("Error reporting dexcount output; please clean and rebuild.", e);
050        }
051    }
052
053    private void actuallyExecute() throws IOException {
054        TreeGenOutput treeGen = readTreeGenFile();
055        if (treeGen.tree == null) {
056            LOGGER.error("Corrupted dexcount data; please clean and rebuild.");
057            return;
058        }
059
060        String inputRepresentation = treeGen.inputRepresentation;
061        if (inputRepresentation == null) {
062            LOGGER.error("Corrupted dexcount data; please clean and rebuild.");
063            return;
064        }
065
066        PackageTree tree = PackageTree.fromThrift(treeGen.tree);
067        CountReporter reporter = new CountReporter(
068            tree,
069            getParameters().getVariantName().get(),
070            new Slf4jStyleable(LOGGER),
071            getParameters().getPrintOptions().get(),
072            inputRepresentation,
073            false);
074
075        reporter.report();
076    }
077
078    private TreeGenOutput readTreeGenFile() throws IOException {
079        File file = getParameters().getPackageTreeFile().getAsFile().get();
080        BufferedSource source = Okio.buffer(new GzipSource(Okio.source(file)));
081        Transport transport = new Transport() {
082            @Override
083            public int read(byte[] buffer, int offset, int count) throws IOException {
084                return source.read(buffer, offset, count);
085            }
086
087            @Override
088            public void write(byte[] buffer, int offset, int count) {
089                throw new UnsupportedOperationException();
090            }
091
092            @Override
093            public void flush() {
094                throw new UnsupportedOperationException();
095            }
096
097            @Override
098            public void close() throws IOException {
099                source.close();
100            }
101        };
102        Protocol protocol = new CompactProtocol(transport);
103        return TreeGenOutput.ADAPTER.read(protocol);
104    }
105
106    private static class Slf4jStyleable implements Styleable {
107        private final Logger logger;
108
109        Slf4jStyleable(Logger logger) {
110            this.logger = logger;
111        }
112
113        @Override
114        public void withStyledOutput(Color color, LogLevel level, IOConsumer<PrintWriter> fn) throws IOException {
115            LoggerWriterAdapter adapter = new LoggerWriterAdapter(logger, color, level);
116            try (PrintWriter pw = new PrintWriter(adapter)) {
117                fn.accept(pw);
118                pw.flush();
119            }
120        }
121    }
122
123    private static class LoggerWriterAdapter extends Writer {
124        private final Logger logger;
125        private final Color color;
126        private final LogLevel logLevel;
127
128        LoggerWriterAdapter(Logger logger, Color color, LogLevel logLevel) {
129            this.logger = logger;
130            this.color = color;
131            this.logLevel = logLevel != null ? logLevel : LogLevel.WARN;
132        }
133
134        @Override
135        public void write(@NotNull char[] chars, int length, int offset) throws IOException {
136            String message = new String(chars, length, offset);
137            if (System.lineSeparator().equals(message)) {
138                // Dumb hack to work around PrintWriter.println breaking Gradle's stdout
139                // as mediated through SLF4J.
140                //
141                // Basically, this completely eliminates the newline from println calls
142                // and relies on SLF4J's (Gradle's?) behavior of appending the newline.
143                //
144                // Without this hack, one println results in three or four newlines.
145                return;
146            }
147
148            switch (logLevel) {
149                case DEBUG:
150                    logger.debug(message);
151                    break;
152
153                case INFO:
154                case LIFECYCLE:
155                    logger.info(message);
156                    break;
157
158                case WARN:
159                case QUIET:
160                    logger.warn(message);
161                    break;
162
163                case ERROR:
164                    logger.error(message);
165                    break;
166            }
167        }
168
169        @Override
170        public void flush() {
171
172        }
173
174        @Override
175        public void close() {
176
177        }
178    }
179}