    001/*
    002 *  Licensed to the Apache Software Foundation (ASF) under one or more
    003 *  contributor license agreements.  See the NOTICE file distributed with
    004 *  this work for additional information regarding copyright ownership.
    005 *  The ASF licenses this file to You under the Apache License, Version 2.0
    006 *  (the "License"); you may not use this file except in compliance with
    007 *  the License.  You may obtain a copy of the License at
    008 *
    009 *     http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 *  Unless required by applicable law or agreed to in writing, software
    012 *  distributed under the License is distributed on an "AS IS" BASIS,
    013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 *  See the License for the specific language governing permissions and
    015 *  limitations under the License.
    016 */
    017package org.apache.commons.compress.harmony.unpack200;
    018
    019import java.io.BufferedInputStream;
    020import java.io.ByteArrayInputStream;
    021import java.io.ByteArrayOutputStream;
    022import java.io.DataOutputStream;
    023import java.io.IOException;
    024import java.io.InputStream;
    025import java.io.OutputStream;
    026import java.io.PrintWriter;
    027import java.util.ArrayList;
    028import java.util.HashSet;
    029import java.util.List;
    030import java.util.Set;
    031import java.util.TimeZone;
    032import java.util.jar.JarEntry;
    033import java.util.jar.JarOutputStream;
    034import java.util.zip.CRC32;
    035import java.util.zip.GZIPInputStream;
    036import java.util.zip.ZipEntry;
    037
    038import org.apache.commons.compress.harmony.pack200.Codec;
    039import org.apache.commons.compress.harmony.pack200.Pack200Exception;
    040import org.apache.commons.compress.harmony.unpack200.bytecode.Attribute;
    041import org.apache.commons.compress.harmony.unpack200.bytecode.CPClass;
    042import org.apache.commons.compress.harmony.unpack200.bytecode.CPField;
    043import org.apache.commons.compress.harmony.unpack200.bytecode.CPMethod;
    044import org.apache.commons.compress.harmony.unpack200.bytecode.CPUTF8;
    045import org.apache.commons.compress.harmony.unpack200.bytecode.ClassConstantPool;
    046import org.apache.commons.compress.harmony.unpack200.bytecode.ClassFile;
    047import org.apache.commons.compress.harmony.unpack200.bytecode.ClassFileEntry;
    048import org.apache.commons.compress.harmony.unpack200.bytecode.InnerClassesAttribute;
    049import org.apache.commons.compress.harmony.unpack200.bytecode.SourceFileAttribute;
    050
    051/**
    052 * A Pack200 archive consists of one or more segments. Each segment is stand-alone, in the sense that every segment has
    053 * the magic number header; thus, every segment is also a valid archive. However, it is possible to combine
    054 * (non-GZipped) archives into a single large archive by concatenation alone. Thus all the hard work in unpacking an
    055 * archive falls to understanding a segment.
    056 *
    057 * The first component of a segment is the header; this contains (amongst other things) the expected counts of constant
    058 * pool entries, which in turn defines how many values need to be read from the stream. Because values are variable
    059 * width (see {@link Codec}), it is not possible to calculate the start of the next segment, although one of the header
    060 * values does hint at the size of the segment if non-zero, which can be used for buffering purposes.
    061 *
    062 * Note that this does not perform any buffering of the input stream; each value will be read on a byte-by-byte basis.
    063 * It does not perform GZip decompression automatically; both of these are expected to be done by the caller if the
    064 * stream has the magic header for GZip streams ({@link GZIPInputStream#GZIP_MAGIC}). In any case, if GZip decompression
    065 * is being performed the input stream will be buffered at a higher level, and thus this can read on a byte-oriented
    066 * basis.
    067 */
    068public class Segment {
    069
    070    public static final int LOG_LEVEL_VERBOSE = 2;
    071
    072    public static final int LOG_LEVEL_STANDARD = 1;
    073
    074    public static final int LOG_LEVEL_QUIET = 0;
    075
    076    private SegmentHeader header;
    077
    078    private CpBands cpBands;
    079
    080    private AttrDefinitionBands attrDefinitionBands;
    081
    082    private IcBands icBands;
    083
    084    private ClassBands classBands;
    085
    086    private BcBands bcBands;
    087
    088    private FileBands fileBands;
    089
    090    private boolean overrideDeflateHint;
    091
    092    private boolean deflateHint;
    093
    094    private boolean doPreRead;
    095
    096    private int logLevel;
    097
    098    private PrintWriter logStream;
    099
    100    private byte[][] classFilesContents;
    101
    102    private boolean[] fileDeflate;
    103
    104    private boolean[] fileIsClass;
    105
    106    private InputStream internalBuffer;
    107
    108    private ClassFile buildClassFile(final int classNum) throws Pack200Exception {
    109        final ClassFile classFile = new ClassFile();
    110        final int[] major = classBands.getClassVersionMajor();
    111        final int[] minor = classBands.getClassVersionMinor();
    112        if (major != null) {
    113            classFile.major = major[classNum];
    114            classFile.minor = minor[classNum];
    115        } else {
    116            classFile.major = header.getDefaultClassMajorVersion();
    117            classFile.minor = header.getDefaultClassMinorVersion();
    118        }
    119        // build constant pool
    120        final ClassConstantPool cp = classFile.pool;
    121        final int fullNameIndexInCpClass = classBands.getClassThisInts()[classNum];
    122        final String fullName = cpBands.getCpClass()[fullNameIndexInCpClass];
    123        // SourceFile attribute
    124        int i = fullName.lastIndexOf("/") + 1; // if lastIndexOf==-1, then
    125        // -1+1=0, so str.substring(0)
    126        // == str
    127
    128        // Get the source file attribute
    129        final ArrayList classAttributes = classBands.getClassAttributes()[classNum];
    130        SourceFileAttribute sourceFileAttribute = null;
    131        for (int index = 0; index < classAttributes.size(); index++) {
    132            if (((Attribute) classAttributes.get(index)).isSourceFileAttribute()) {
    133                sourceFileAttribute = ((SourceFileAttribute) classAttributes.get(index));
    134            }
    135        }
    136
    137        if (sourceFileAttribute == null) {
    138            // If we don't have a source file attribute yet, we need
    139            // to infer it from the class.
    140            final AttributeLayout SOURCE_FILE = attrDefinitionBands.getAttributeDefinitionMap()
    141                .getAttributeLayout(AttributeLayout.ATTRIBUTE_SOURCE_FILE, AttributeLayout.CONTEXT_CLASS);
    142            if (SOURCE_FILE.matches(classBands.getRawClassFlags()[classNum])) {
    143                int firstDollar = -1;
    144                for (int index = 0; index < fullName.length(); index++) {
    145                    if (fullName.charAt(index) <= '$') {
    146                        firstDollar = index;
    147                    }
    148                }
    149                String fileName = null;
    150
    151                if (firstDollar > -1 && (i <= firstDollar)) {
    152                    fileName = fullName.substring(i, firstDollar) + ".java";
    153                } else {
    154                    fileName = fullName.substring(i) + ".java";
    155                }
    156                sourceFileAttribute = new SourceFileAttribute(cpBands.cpUTF8Value(fileName, false));
    157                classFile.attributes = new Attribute[] {(Attribute) cp.add(sourceFileAttribute)};
    158            } else {
    159                classFile.attributes = new Attribute[] {};
    160            }
    161        } else {
    162            classFile.attributes = new Attribute[] {(Attribute) cp.add(sourceFileAttribute)};
    163        }
    164
    165        // If we see any class attributes, add them to the class's attributes
    166        // that will
    167        // be written out. Keep SourceFileAttributes out since we just
    168        // did them above.
    169        final ArrayList classAttributesWithoutSourceFileAttribute = new ArrayList(classAttributes.size());
    170        for (int index = 0; index < classAttributes.size(); index++) {
    171            final Attribute attrib = (Attribute) classAttributes.get(index);
    172            if (!attrib.isSourceFileAttribute()) {
    173                classAttributesWithoutSourceFileAttribute.add(attrib);
    174            }
    175        }
    176        final Attribute[] originalAttributes = classFile.attributes;
    177        classFile.attributes = new Attribute[originalAttributes.length
    178            + classAttributesWithoutSourceFileAttribute.size()];
    179        System.arraycopy(originalAttributes, 0, classFile.attributes, 0, originalAttributes.length);
    180        for (int index = 0; index < classAttributesWithoutSourceFileAttribute.size(); index++) {
    181            final Attribute attrib = ((Attribute) classAttributesWithoutSourceFileAttribute.get(index));
    182            cp.add(attrib);
    183            classFile.attributes[originalAttributes.length + index] = attrib;
    184        }
    185
    186        // this/superclass
    187        final ClassFileEntry cfThis = cp.add(cpBands.cpClassValue(fullNameIndexInCpClass));
    188        final ClassFileEntry cfSuper = cp.add(cpBands.cpClassValue(classBands.getClassSuperInts()[classNum]));
    189        // add interfaces
    190        final ClassFileEntry cfInterfaces[] = new ClassFileEntry[classBands.getClassInterfacesInts()[classNum].length];
    191        for (i = 0; i < cfInterfaces.length; i++) {
    192            cfInterfaces[i] = cp.add(cpBands.cpClassValue(classBands.getClassInterfacesInts()[classNum][i]));
    193        }
    194        // add fields
    195        final ClassFileEntry cfFields[] = new ClassFileEntry[classBands.getClassFieldCount()[classNum]];
    196        // fieldDescr and fieldFlags used to create this
    197        for (i = 0; i < cfFields.length; i++) {
    198            final int descriptorIndex = classBands.getFieldDescrInts()[classNum][i];
    199            final int nameIndex = cpBands.getCpDescriptorNameInts()[descriptorIndex];
    200            final int typeIndex = cpBands.getCpDescriptorTypeInts()[descriptorIndex];
    201            final CPUTF8 name = cpBands.cpUTF8Value(nameIndex);
    202            final CPUTF8 descriptor = cpBands.cpSignatureValue(typeIndex);
    203            cfFields[i] = cp.add(new CPField(name, descriptor, classBands.getFieldFlags()[classNum][i],
    204                classBands.getFieldAttributes()[classNum][i]));
    205        }
    206        // add methods
    207        final ClassFileEntry cfMethods[] = new ClassFileEntry[classBands.getClassMethodCount()[classNum]];
    208        // methodDescr and methodFlags used to create this
    209        for (i = 0; i < cfMethods.length; i++) {
    210            final int descriptorIndex = classBands.getMethodDescrInts()[classNum][i];
    211            final int nameIndex = cpBands.getCpDescriptorNameInts()[descriptorIndex];
    212            final int typeIndex = cpBands.getCpDescriptorTypeInts()[descriptorIndex];
    213            final CPUTF8 name = cpBands.cpUTF8Value(nameIndex);
    214            final CPUTF8 descriptor = cpBands.cpSignatureValue(typeIndex);
    215            cfMethods[i] = cp.add(new CPMethod(name, descriptor, classBands.getMethodFlags()[classNum][i],
    216                classBands.getMethodAttributes()[classNum][i]));
    217        }
    218        cp.addNestedEntries();
    219
    220        // add inner class attribute (if required)
    221        boolean addInnerClassesAttr = false;
    222        final IcTuple[] ic_local = getClassBands().getIcLocal()[classNum];
    223        final boolean ic_local_sent = ic_local != null;
    224        final InnerClassesAttribute innerClassesAttribute = new InnerClassesAttribute("InnerClasses");
    225        final IcTuple[] ic_relevant = getIcBands().getRelevantIcTuples(fullName, cp);
    226        final List ic_stored = computeIcStored(ic_local, ic_relevant);
    227        for (int index = 0; index < ic_stored.size(); index++) {
    228            final IcTuple icStored = (IcTuple) ic_stored.get(index);
    229            final int innerClassIndex = icStored.thisClassIndex();
    230            final int outerClassIndex = icStored.outerClassIndex();
    231            final int simpleClassNameIndex = icStored.simpleClassNameIndex();
    232
    233            final String innerClassString = icStored.thisClassString();
    234            final String outerClassString = icStored.outerClassString();
    235            final String simpleClassName = icStored.simpleClassName();
    236
    237            CPClass innerClass = null;
    238            CPUTF8 innerName = null;
    239            CPClass outerClass = null;
    240
    241            innerClass = innerClassIndex != -1 ? cpBands.cpClassValue(innerClassIndex)
    242                : cpBands.cpClassValue(innerClassString);
    243            if (!icStored.isAnonymous()) {
    244                innerName = simpleClassNameIndex != -1 ? cpBands.cpUTF8Value(simpleClassNameIndex)
    245                    : cpBands.cpUTF8Value(simpleClassName);
    246            }
    247
    248            if (icStored.isMember()) {
    249                outerClass = outerClassIndex != -1 ? cpBands.cpClassValue(outerClassIndex)
    250                    : cpBands.cpClassValue(outerClassString);
    251            }
    252            final int flags = icStored.F;
    253            innerClassesAttribute.addInnerClassesEntry(innerClass, outerClass, innerName, flags);
    254            addInnerClassesAttr = true;
    255        }
    256        // If ic_local is sent and it's empty, don't add
    257        // the inner classes attribute.
    258        if (ic_local_sent && (ic_local.length == 0)) {
    259            addInnerClassesAttr = false;
    260        }
    261
    262        // If ic_local is not sent and ic_relevant is empty,
    263        // don't add the inner class attribute.
    264        if (!ic_local_sent && (ic_relevant.length == 0)) {
    265            addInnerClassesAttr = false;
    266        }
    267
    268        if (addInnerClassesAttr) {
    269            // Need to add the InnerClasses attribute to the
    270            // existing classFile attributes.
    271            final Attribute[] originalAttrs = classFile.attributes;
    272            final Attribute[] newAttrs = new Attribute[originalAttrs.length + 1];
    273            for (int index = 0; index < originalAttrs.length; index++) {
    274                newAttrs[index] = originalAttrs[index];
    275            }
    276            newAttrs[newAttrs.length - 1] = innerClassesAttribute;
    277            classFile.attributes = newAttrs;
    278            cp.addWithNestedEntries(innerClassesAttribute);
    279        }
    280        // sort CP according to cp_All
    281        cp.resolve(this);
    282        // NOTE the indexOf is only valid after the cp.resolve()
    283        // build up remainder of file
    284        classFile.accessFlags = (int) classBands.getClassFlags()[classNum];
    285        classFile.thisClass = cp.indexOf(cfThis);
    286        classFile.superClass = cp.indexOf(cfSuper);
    287        // TODO placate format of file for writing purposes
    288        classFile.interfaces = new int[cfInterfaces.length];
    289        for (i = 0; i < cfInterfaces.length; i++) {
    290            classFile.interfaces[i] = cp.indexOf(cfInterfaces[i]);
    291        }
    292        classFile.fields = cfFields;
    293        classFile.methods = cfMethods;
    294        return classFile;
    295    }
    296
    297    /**
    298     * Given an ic_local and an ic_relevant, use them to calculate what should be added as ic_stored.
    299     *
    300     * @param ic_local IcTuple[] array of local transmitted tuples
    301     * @param ic_relevant IcTuple[] array of relevant tuples
    302     * @return List of tuples to be stored. If ic_local is null or empty, the values returned may not be correct. The
    303     *         caller will have to determine if this is the case.
    304     */
    305    private List computeIcStored(final IcTuple[] ic_local, final IcTuple[] ic_relevant) {
    306        final List result = new ArrayList(ic_relevant.length);
    307        final List duplicates = new ArrayList(ic_relevant.length);
    308        final Set isInResult = new HashSet(ic_relevant.length);
    309
    310        // need to compute:
    311        // result = ic_local XOR ic_relevant
    312
    313        // add ic_local
    314        if (ic_local != null) {
    315            for (int index = 0; index < ic_local.length; index++) {
    316                if (isInResult.add(ic_local[index])) {
    317                    result.add(ic_local[index]);
    318                }
    319            }
    320        }
    321
    322        // add ic_relevant
    323        for (int index = 0; index < ic_relevant.length; index++) {
    324            if (isInResult.add(ic_relevant[index])) {
    325                result.add(ic_relevant[index]);
    326            } else {
    327                duplicates.add(ic_relevant[index]);
    328            }
    329        }
    330
    331        // eliminate "duplicates"
    332        for (int index = 0; index < duplicates.size(); index++) {
    333            final IcTuple tuple = (IcTuple) duplicates.get(index);
    334            result.remove(tuple);
    335        }
    336
    337        return result;
    338    }
    339
    340    /**
    341     * This performs reading the data from the stream into non-static instance of Segment. After the completion of this
    342     * method stream can be freed.
    343     *
    344     * @param in the input stream to read from
    345     * @throws IOException if a problem occurs during reading from the underlying stream
    346     * @throws Pack200Exception if a problem occurs with an unexpected value or unsupported codec
    347     */
    348    private void readSegment(final InputStream in) throws IOException, Pack200Exception {
    349        log(LOG_LEVEL_VERBOSE, "-------");
    350        cpBands = new CpBands(this);
    351        cpBands.read(in);
    352        attrDefinitionBands = new AttrDefinitionBands(this);
    353        attrDefinitionBands.read(in);
    354        icBands = new IcBands(this);
    355        icBands.read(in);
    356        classBands = new ClassBands(this);
    357        classBands.read(in);
    358        bcBands = new BcBands(this);
    359        bcBands.read(in);
    360        fileBands = new FileBands(this);
    361        fileBands.read(in);
    362
    363        fileBands.processFileBits();
    364    }
    365
    366    /**
    367     * This performs the actual work of parsing against a non-static instance of Segment. This method is intended to run
    368     * concurrently for multiple segments.
    369     *
    370     * @throws IOException if a problem occurs during reading from the underlying stream
    371     * @throws Pack200Exception if a problem occurs with an unexpected value or unsupported codec
    372     */
    373    private void parseSegment() throws IOException, Pack200Exception {
    374
    375        header.unpack();
    376        cpBands.unpack();
    377        attrDefinitionBands.unpack();
    378        icBands.unpack();
    379        classBands.unpack();
    380        bcBands.unpack();
    381        fileBands.unpack();
    382
    383        int classNum = 0;
    384        final int numberOfFiles = header.getNumberOfFiles();
    385        final String[] fileName = fileBands.getFileName();
    386        final int[] fileOptions = fileBands.getFileOptions();
    387        final SegmentOptions options = header.getOptions();
    388
    389        classFilesContents = new byte[numberOfFiles][];
    390        fileDeflate = new boolean[numberOfFiles];
    391        fileIsClass = new boolean[numberOfFiles];
    392
    393        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    394        final DataOutputStream dos = new DataOutputStream(bos);
    395
    396        for (int i = 0; i < numberOfFiles; i++) {
    397            String name = fileName[i];
    398
    399            final boolean nameIsEmpty = (name == null) || name.equals("");
    400            final boolean isClass = (fileOptions[i] & 2) == 2 || nameIsEmpty;
    401            if (isClass && nameIsEmpty) {
    402                name = cpBands.getCpClass()[classBands.getClassThisInts()[classNum]] + ".class";
    403                fileName[i] = name;
    404            }
    405
    406            if (!overrideDeflateHint) {
    407                fileDeflate[i] = (fileOptions[i] & 1) == 1 || options.shouldDeflate();
    408            } else {
    409                fileDeflate[i] = deflateHint;
    410            }
    411
    412            fileIsClass[i] = isClass;
    413
    414            if (isClass) {
    415                final ClassFile classFile = buildClassFile(classNum);
    416                classFile.write(dos);
    417                dos.flush();
    418
    419                classFilesContents[classNum] = bos.toByteArray();
    420                bos.reset();
    421
    422                classNum++;
    423            }
    424        }
    425    }
    426
    427    /**
    428     * Unpacks a packed stream (either .pack. or .pack.gz) into a corresponding JarOuputStream.
    429     *
    430     * @param in a packed stream.
    431     * @param out output stream.
    432     * @throws Pack200Exception if there is a problem unpacking
    433     * @throws IOException if there is a problem with I/O during unpacking
    434     */
    435    public void unpack(final InputStream in, final JarOutputStream out) throws IOException, Pack200Exception {
    436        unpackRead(in);
    437        unpackProcess();
    438        unpackWrite(out);
    439    }
    440
    441    /*
    442     * Package-private accessors for unpacking stages
    443     */
    444    void unpackRead(InputStream in) throws IOException, Pack200Exception {
    445        if (!in.markSupported()) {
    446            in = new BufferedInputStream(in);
    447        }
    448
    449        header = new SegmentHeader(this);
    450        header.read(in);
    451
    452        final int size = (int) header.getArchiveSize() - header.getArchiveSizeOffset();
    453
    454        if (doPreRead && header.getArchiveSize() != 0) {
    455            final byte[] data = new byte[size];
    456            in.read(data);
    457            internalBuffer = new BufferedInputStream(new ByteArrayInputStream(data));
    458        } else {
    459            readSegment(in);
    460        }
    461    }
    462
    463    void unpackProcess() throws IOException, Pack200Exception {
    464        if (internalBuffer != null) {
    465            readSegment(internalBuffer);
    466        }
    467        parseSegment();
    468    }
    469
    470    void unpackWrite(final JarOutputStream out) throws IOException, Pack200Exception {
    471        writeJar(out);
    472        if (logStream != null) {
    473            logStream.close();
    474        }
    475    }
    476
    477    /**
    478     * Writes the segment to an output stream. The output stream should be pre-buffered for efficiency. Also takes the
    479     * same input stream for reading, since the file bits may not be loaded and thus just copied from one stream to
    480     * another. Doesn't close the output stream when finished, in case there are more entries (e.g. further segments) to
    481     * be written.
    482     *
    483     * @param out the JarOutputStream to write data to
    484     * @throws IOException if an error occurs while reading or writing to the streams
    485     * @throws Pack200Exception if an error occurs while processing data
    486     */
    487    public void writeJar(final JarOutputStream out) throws IOException, Pack200Exception {
    488        final String[] fileName = fileBands.getFileName();
    489        final int[] fileModtime = fileBands.getFileModtime();
    490        final long[] fileSize = fileBands.getFileSize();
    491        final byte[][] fileBits = fileBands.getFileBits();
    492
    493        // now write the files out
    494        int classNum = 0;
    495        final int numberOfFiles = header.getNumberOfFiles();
    496        final long archiveModtime = header.getArchiveModtime();
    497
    498        for (int i = 0; i < numberOfFiles; i++) {
    499            final String name = fileName[i];
    500            // For Pack200 archives, modtime is in seconds
    501            // from the epoch. JarEntries need it to be in
    502            // milliseconds from the epoch.
    503            // Even though we're adding two longs and multiplying
    504            // by 1000, we won't overflow because both longs are
    505            // always under 2^32.
    506            final long modtime = 1000 * (archiveModtime + fileModtime[i]);
    507            final boolean deflate = fileDeflate[i];
    508
    509            final JarEntry entry = new JarEntry(name);
    510            if (deflate) {
    511                entry.setMethod(ZipEntry.DEFLATED);
    512            } else {
    513                entry.setMethod(ZipEntry.STORED);
    514                final CRC32 crc = new CRC32();
    515                if (fileIsClass[i]) {
    516                    crc.update(classFilesContents[classNum]);
    517                    entry.setSize(classFilesContents[classNum].length);
    518                } else {
    519                    crc.update(fileBits[i]);
    520                    entry.setSize(fileSize[i]);
    521                }
    522                entry.setCrc(crc.getValue());
    523            }
    524            // On Windows at least, need to correct for timezone
    525            entry.setTime(modtime - TimeZone.getDefault().getRawOffset());
    526            out.putNextEntry(entry);
    527
    528            // write to output stream
    529            if (fileIsClass[i]) {
    530                entry.setSize(classFilesContents[classNum].length);
    531                out.write(classFilesContents[classNum]);
    532                classNum++;
    533            } else {
    534                entry.setSize(fileSize[i]);
    535                out.write(fileBits[i]);
    536            }
    537        }
    538    }
    539
    540    public SegmentConstantPool getConstantPool() {
    541        return cpBands.getConstantPool();
    542    }
    543
    544    public SegmentHeader getSegmentHeader() {
    545        return header;
    546    }
    547
    548    public void setPreRead(final boolean value) {
    549        doPreRead = value;
    550    }
    551
    552    protected AttrDefinitionBands getAttrDefinitionBands() {
    553        return attrDefinitionBands;
    554    }
    555
    556    protected ClassBands getClassBands() {
    557        return classBands;
    558    }
    559
    560    protected CpBands getCpBands() {
    561        return cpBands;
    562    }
    563
    564    protected IcBands getIcBands() {
    565        return icBands;
    566    }
    567
    568    public void setLogLevel(final int logLevel) {
    569        this.logLevel = logLevel;
    570    }
    571
    572    public void setLogStream(final OutputStream logStream) {
    573        this.logStream = new PrintWriter(logStream);
    574    }
    575
    576    public void log(final int logLevel, final String message) {
    577        if (this.logLevel >= logLevel) {
    578            logStream.println(message);
    579        }
    580    }
    581
    582    /**
    583     * Override the archive's deflate hint with the given boolean
    584     *
    585     * @param deflateHint - the deflate hint to use
    586     */
    587    public void overrideDeflateHint(final boolean deflateHint) {
    588        this.overrideDeflateHint = true;
    589        this.deflateHint = deflateHint;
    590    }
    591
    592}