001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2008 Sun Microsystems, Inc.
025 *      Portions Copyright 2015 ForgeRock AS.
026 */
027package org.forgerock.opendj.config;
028
029import java.util.HashMap;
030import java.util.Map;
031import java.util.regex.Matcher;
032import java.util.regex.Pattern;
033
034/**
035 * This enumeration defines various memory size units.
036 */
037public enum SizeUnit {
038
039    /** A byte unit. */
040    BYTES(1L, "b", "bytes"),
041
042    /** A gibi-byte unit. */
043    GIBI_BYTES(1024L * 1024 * 1024, "gib", "gibibytes"),
044
045    /** A giga-byte unit. */
046    GIGA_BYTES(1000L * 1000 * 1000, "gb", "gigabytes"),
047
048    /** A kibi-byte unit. */
049    KIBI_BYTES(1024L, "kib", "kibibytes"),
050
051    /** A kilo-byte unit. */
052    KILO_BYTES(1000L, "kb", "kilobytes"),
053
054    /** A mebi-byte unit. */
055    MEBI_BYTES(1024L * 1024, "mib", "mebibytes"),
056
057    /** A mega-byte unit. */
058    MEGA_BYTES(1000L * 1000, "mb", "megabytes"),
059
060    /** A tebi-byte unit. */
061    TEBI_BYTES(1024L * 1024 * 1024 * 1024, "tib", "tebibytes"),
062
063    /** A tera-byte unit. */
064    TERA_BYTES(1000L * 1000 * 1000 * 1000, "tb", "terabytes");
065
066    /** A lookup table for resolving a unit from its name. */
067    private static final Map<String, SizeUnit> NAME_TO_UNIT = new HashMap<>();
068    static {
069        for (SizeUnit unit : SizeUnit.values()) {
070            NAME_TO_UNIT.put(unit.shortName, unit);
071            NAME_TO_UNIT.put(unit.longName, unit);
072        }
073    }
074
075    /**
076     * Gets the best-fit unit for the specified number of bytes. The returned
077     * unit will be able to represent the number of bytes using a decimal number
078     * comprising of an integer part which is greater than zero. Bigger units
079     * are chosen in preference to smaller units and binary units are only
080     * returned if they are an exact fit. If the number of bytes is zero then
081     * the {@link #BYTES} unit is always returned. For example:
082     *
083     * <pre>
084     * getBestFitUnit(0)       // BYTES
085     * getBestFitUnit(999)     // BYTES
086     * getBestFitUnit(1000)    // KILO_BYTES
087     * getBestFitUnit(1024)    // KIBI_BYTES
088     * getBestFitUnit(1025)    // KILO_BYTES
089     * getBestFitUnit(999999)  // KILO_BYTES
090     * getBestFitUnit(1000000) // MEGA_BYTES
091     * </pre>
092     *
093     * @param bytes
094     *            The number of bytes.
095     * @return Returns the best fit unit.
096     * @throws IllegalArgumentException
097     *             If <code>bytes</code> is negative.
098     * @see #getBestFitUnitExact(long)
099     */
100    public static SizeUnit getBestFitUnit(long bytes) {
101        if (bytes < 0) {
102            throw new IllegalArgumentException("negative number of bytes: " + bytes);
103        } else if (bytes == 0) {
104            // Always use bytes for zero values.
105            return BYTES;
106        } else {
107            // Determine best fit: prefer non-binary units unless binary
108            // fits exactly.
109            SizeUnit[] nonBinary = new SizeUnit[] { TERA_BYTES, GIGA_BYTES, MEGA_BYTES, KILO_BYTES };
110            SizeUnit[] binary = new SizeUnit[] { TEBI_BYTES, GIBI_BYTES, MEBI_BYTES, KIBI_BYTES };
111
112            for (int i = 0; i < nonBinary.length; i++) {
113                if ((bytes % binary[i].getSize()) == 0) {
114                    return binary[i];
115                } else if ((bytes / nonBinary[i].getSize()) > 0) {
116                    return nonBinary[i];
117                }
118            }
119
120            return BYTES;
121        }
122    }
123
124    /**
125     * Gets the best-fit unit for the specified number of bytes which can
126     * represent the provided value using an integral value. Bigger units are
127     * chosen in preference to smaller units. If the number of bytes is zero
128     * then the {@link #BYTES} unit is always returned. For example:
129     *
130     * <pre>
131     * getBestFitUnitExact(0)       // BYTES
132     * getBestFitUnitExact(999)     // BYTES
133     * getBestFitUnitExact(1000)    // KILO_BYTES
134     * getBestFitUnitExact(1024)    // KIBI_BYTES
135     * getBestFitUnitExact(1025)    // BYTES
136     * getBestFitUnitExact(999999)  // BYTES
137     * getBestFitUnitExact(1000000) // MEGA_BYTES
138     * </pre>
139     *
140     * @param bytes
141     *            The number of bytes.
142     * @return Returns the best fit unit can represent the provided value using
143     *         an integral value.
144     * @throws IllegalArgumentException
145     *             If <code>bytes</code> is negative.
146     * @see #getBestFitUnit(long)
147     */
148    public static SizeUnit getBestFitUnitExact(long bytes) {
149        if (bytes < 0) {
150            throw new IllegalArgumentException("negative number of bytes: " + bytes);
151        } else if (bytes == 0) {
152            // Always use bytes for zero values.
153            return BYTES;
154        } else {
155            // Determine best fit.
156            SizeUnit[] units =
157                new SizeUnit[] { TEBI_BYTES, TERA_BYTES, GIBI_BYTES, GIGA_BYTES, MEBI_BYTES, MEGA_BYTES, KIBI_BYTES,
158                    KILO_BYTES };
159
160            for (SizeUnit unit : units) {
161                if ((bytes % unit.getSize()) == 0) {
162                    return unit;
163                }
164            }
165
166            return BYTES;
167        }
168    }
169
170    /**
171     * Get the unit corresponding to the provided unit name.
172     *
173     * @param s
174     *            The name of the unit. Can be the abbreviated or long name and
175     *            can contain white space and mixed case characters.
176     * @return Returns the unit corresponding to the provided unit name.
177     * @throws IllegalArgumentException
178     *             If the provided name did not correspond to a known memory
179     *             size unit.
180     */
181    public static SizeUnit getUnit(String s) {
182        SizeUnit unit = NAME_TO_UNIT.get(s.trim().toLowerCase());
183        if (unit == null) {
184            throw new IllegalArgumentException("Illegal memory size unit \"" + s + "\"");
185        }
186        return unit;
187    }
188
189    /**
190     * Parse the provided size string and return its equivalent size in bytes.
191     * The size string must specify the unit e.g. "10kb".
192     *
193     * @param s
194     *            The size string to be parsed.
195     * @return Returns the parsed duration in bytes.
196     * @throws NumberFormatException
197     *             If the provided size string could not be parsed.
198     */
199    public static long parseValue(String s) {
200        return parseValue(s, null);
201    }
202
203    /**
204     * Parse the provided size string and return its equivalent size in bytes.
205     *
206     * @param s
207     *            The size string to be parsed.
208     * @param defaultUnit
209     *            The default unit to use if there is no unit specified in the
210     *            size string, or <code>null</code> if the string must always
211     *            contain a unit.
212     * @return Returns the parsed size in bytes.
213     * @throws NumberFormatException
214     *             If the provided size string could not be parsed.
215     */
216    public static long parseValue(String s, SizeUnit defaultUnit) {
217        // Value must be a floating point number followed by a unit.
218        Pattern p = Pattern.compile("^\\s*(\\d+(\\.\\d+)?)\\s*(\\w+)?\\s*$");
219        Matcher m = p.matcher(s);
220
221        if (!m.matches()) {
222            throw new NumberFormatException("Invalid size value \"" + s + "\"");
223        }
224
225        // Group 1 is the float.
226        double d;
227        try {
228            d = Double.valueOf(m.group(1));
229        } catch (NumberFormatException e) {
230            throw new NumberFormatException("Invalid size value \"" + s + "\"");
231        }
232
233        // Group 3 is the unit.
234        String unitString = m.group(3);
235        SizeUnit unit;
236        if (unitString == null) {
237            if (defaultUnit == null) {
238                throw new NumberFormatException("Invalid size value \"" + s + "\"");
239            } else {
240                unit = defaultUnit;
241            }
242        } else {
243            try {
244                unit = getUnit(unitString);
245            } catch (IllegalArgumentException e) {
246                throw new NumberFormatException("Invalid size value \"" + s + "\"");
247            }
248        }
249
250        return unit.toBytes(d);
251    }
252
253    /** The long name of the unit. */
254    private final String longName;
255
256    /** The abbreviation of the unit. */
257    private final String shortName;
258
259    /** The size of the unit in bytes. */
260    private final long sz;
261
262    /** Private constructor. */
263    private SizeUnit(long sz, String shortName, String longName) {
264        this.sz = sz;
265        this.shortName = shortName;
266        this.longName = longName;
267    }
268
269    /**
270     * Converts the specified size in bytes to this unit.
271     *
272     * @param amount
273     *            The size in bytes.
274     * @return Returns size in this unit.
275     */
276    public double fromBytes(long amount) {
277        return (double) amount / sz;
278    }
279
280    /**
281     * Get the long name of this unit.
282     *
283     * @return Returns the long name of this unit.
284     */
285    public String getLongName() {
286        return longName;
287    }
288
289    /**
290     * Get the abbreviated name of this unit.
291     *
292     * @return Returns the abbreviated name of this unit.
293     */
294    public String getShortName() {
295        return shortName;
296    }
297
298    /**
299     * Get the number of bytes that this unit represents.
300     *
301     * @return Returns the number of bytes that this unit represents.
302     */
303    public long getSize() {
304        return sz;
305    }
306
307    /**
308     * Converts the specified size in this unit to bytes.
309     *
310     * @param amount
311     *            The size as a quantity of this unit.
312     * @return Returns the number of bytes that the size represents.
313     * @throws NumberFormatException
314     *             If the provided size exceeded long.MAX_VALUE.
315     */
316    public long toBytes(double amount) {
317        double value = sz * amount;
318        if (value > Long.MAX_VALUE) {
319            throw new NumberFormatException("number too big (exceeded long.MAX_VALUE");
320        }
321        return (long) value;
322    }
323
324    /**
325     * {@inheritDoc}
326     * <p>
327     * This implementation returns the abbreviated name of this size unit.
328     */
329    @Override
330    public String toString() {
331        return shortName;
332    }
333}