PopulationCodec.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.commons.compress.harmony.pack200;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

/**
 * A PopulationCodec is a Codec that is well suited to encoding data that shows statistical or repetitive patterns, containing for example a few numbers which
 * are repeated a lot throughout the set, but not necessarily sequentially.
 *
 * @see <a href="https://docs.oracle.com/en/java/javase/13/docs/specs/pack-spec.html">Pack200: A Packed Class Deployment Format For Java Applications</a>
 */
public class PopulationCodec extends Codec {

    private final Codec favoredCodec;
    private Codec tokenCodec;
    private final Codec unfavoredCodec;
    private int l;
    private int[] favored;

    /**
     * Constructs a new PopulationCodec.
     *
     * @param favoredCodec The favored codec.
     * @param tokenCodec The token codec.
     * @param unvaforedCodec The unfavored codec.
     */
    public PopulationCodec(final Codec favoredCodec, final Codec tokenCodec, final Codec unvaforedCodec) {
        this.favoredCodec = favoredCodec;
        this.tokenCodec = tokenCodec;
        this.unfavoredCodec = unvaforedCodec;
    }

    /**
     * Constructs a new PopulationCodec.
     *
     * @param favoredCodec The favored codec.
     * @param l The L value.
     * @param unfavoredCodec The unfavored codec.
     */
    public PopulationCodec(final Codec favoredCodec, final int l, final Codec unfavoredCodec) {
        if (l >= 256 || l <= 0) {
            throw new IllegalArgumentException("L must be between 1..255");
        }
        this.favoredCodec = favoredCodec;
        this.l = l;
        this.unfavoredCodec = unfavoredCodec;
    }

    @Override
    public int decode(final InputStream in) throws IOException, Pack200Exception {
        throw new Pack200Exception("Population encoding does not work unless the number of elements are known");
    }

    @Override
    public int decode(final InputStream in, final long last) throws IOException, Pack200Exception {
        throw new Pack200Exception("Population encoding does not work unless the number of elements are known");
    }

    @Override
    public int[] decodeInts(final int n, final InputStream in) throws IOException, Pack200Exception {
        lastBandLength = 0;
        favored = new int[Pack200Exception.checkIntArray(check(n, in))]; // there must be <= n values, but probably a lot
        // less
        final int[] result;
        // read table of favorites first
        int smallest = Integer.MAX_VALUE;
        int absoluteSmallest;
        int last = 0;
        int value = 0;
        int absoluteValue;
        int k = -1;
        while (true) {
            value = favoredCodec.decode(in, last);
            if (k > -1 && (value == smallest || value == last)) {
                break;
            }
            favored[++k] = value;
            absoluteSmallest = Math.abs(smallest);
            absoluteValue = Math.abs(value);
            if (absoluteSmallest > absoluteValue) {
                smallest = value;
            } else if (absoluteSmallest == absoluteValue) {
                // ensure that -X and +X -> +X
                smallest = absoluteSmallest;
            }
            last = value;
        }
        lastBandLength += k;
        // if tokenCodec needs to be derived from the T, L and K values
        if (tokenCodec == null) {
            if (k < 256) {
                tokenCodec = BYTE1;
            } else {
                // if k >= 256, b >= 2
                int b = 1;
                BHSDCodec codec;
                while (++b < 5) {
                    codec = new BHSDCodec(b, 256 - l, 0);
                    if (codec.encodes(k)) {
                        tokenCodec = codec;
                        break;
                    }
                }
                Pack200Exception.requireNonNull(tokenCodec, "Cannot calculate token codec from %s and %s", k, l);
            }
        }
        // read favorites
        lastBandLength += n;
        result = tokenCodec.decodeInts(n, in);
        // read unfavorites
        last = 0;
        for (int i = 0; i < n; i++) {
            final int index = result[i];
            if (index == 0) {
                lastBandLength++;
                result[i] = last = unfavoredCodec.decode(in, last);
            } else {
                result[i] = favored[index - 1];
            }
        }
        return result;
    }

    @Override
    public byte[] encode(final int value) throws Pack200Exception {
        throw new Pack200Exception("Population encoding does not work unless the number of elements are known");
    }

    @Override
    public byte[] encode(final int value, final int last) throws Pack200Exception {
        throw new Pack200Exception("Population encoding does not work unless the number of elements are known");
    }

    /**
     * Encodes the values.
     *
     * @param favored The favored values.
     * @param tokens The tokens.
     * @param unfavored The unfavored values.
     * @return The encoded bytes.
     * @throws Pack200Exception if an error occurs.
     */
    public byte[] encode(final int[] favored, final int[] tokens, final int[] unfavored) throws Pack200Exception {
        final int[] favored2 = Arrays.copyOf(favored, favored.length + 1);
        favored2[favored2.length - 1] = favored[favored.length - 1]; // repeat last value;
        final byte[] favoredEncoded = favoredCodec.encode(favored2);
        final byte[] tokensEncoded = tokenCodec.encode(tokens);
        final byte[] unfavoredEncoded = unfavoredCodec.encode(unfavored);
        final byte[] band = new byte[favoredEncoded.length + tokensEncoded.length + unfavoredEncoded.length];
        System.arraycopy(favoredEncoded, 0, band, 0, favoredEncoded.length);
        System.arraycopy(tokensEncoded, 0, band, favoredEncoded.length, tokensEncoded.length);
        System.arraycopy(unfavoredEncoded, 0, band, favoredEncoded.length + tokensEncoded.length, unfavoredEncoded.length);
        return band;
    }

    /**
     * Gets the favored values.
     *
     * @return The favored values.
     */
    public int[] getFavoured() {
        return favored;
    }

    /**
     * Gets the favored codec.
     *
     * @return The favored codec.
     */
    public Codec getFavouredCodec() {
        return favoredCodec;
    }

    /**
     * Gets the token codec.
     *
     * @return The token codec.
     */
    public Codec getTokenCodec() {
        return tokenCodec;
    }

    /**
     * Gets the unfavored codec.
     *
     * @return The unfavored codec.
     */
    public Codec getUnfavouredCodec() {
        return unfavoredCodec;
    }
}