/** * Copyright 2011-2017 Asakusa Framework Team. * * Licensed 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 * * http://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 com.asakusafw.runtime.stage.temporary; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.Arrays; import org.apache.hadoop.io.IOUtils; /** * Utilities for temporary files. * @since 0.7.0 */ public final class TemporaryFile { /** * The block size. */ public static final int BLOCK_SIZE = 64 * 1024 * 1024; /** * The page header size. */ public static final int PAGE_HEADER_SIZE = 3; /** * The page header value which represents {@code END_OF_BLOCK}. */ public static final int PAGE_HEADER_EOB = 0x000000; /** * The page header value which represents {@code END_OF_FILE}. */ public static final int PAGE_HEADER_EOF = -1; /** * The padding byte for empty entries. */ public static final int EMPTY_ENTRY_PADDING = 0; private static final Charset ENCODING = StandardCharsets.UTF_8; /** * The maximum page size. */ public static final int MAX_PAGE_SIZE = 0xffffff; private static final byte[] BLOCK_HEADER = { '`', 'A', 'F', '@' }; private static final int MAJOR_VERSION = 1; private static final ThreadLocal<byte[]> HEADER_BUFFER = ThreadLocal.withInitial(() -> new byte[PAGE_HEADER_SIZE]); private static final ThreadLocal<byte[]> INSTANT_BUFFER = new ThreadLocal<>(); static byte[] getInstantBuffer(int minSize) { byte[] buffer = INSTANT_BUFFER.get(); if (buffer == null || buffer.length < minSize) { int size = (int) (minSize * 1.2); buffer = new byte[size]; INSTANT_BUFFER.set(buffer); } return buffer; } /** * Writes a block header. * @param output the target output * @return the bytes written * @throws IOException if failed to write */ public static int writeBlockHeader(OutputStream output) throws IOException { output.write(BLOCK_HEADER); output.write(MAJOR_VERSION); return BLOCK_HEADER.length + 1; } /** * Reads and verifies the block header. * @param input the target input * @return the bytes read * @throws IOException if failed to read */ public static int readBlockHeader(InputStream input) throws IOException { byte[] header = new byte[BLOCK_HEADER.length]; int offset = 0; while (offset < header.length) { int read = input.read(header, offset, header.length - offset); if (read < 0) { return PAGE_HEADER_EOF; } offset += read; } if (Arrays.equals(header, BLOCK_HEADER) == false) { throw new IOException("Unsupported temporary file format (invalid block header)"); } int version = input.read(); if (version < 0) { return PAGE_HEADER_EOF; } if (version != MAJOR_VERSION) { throw new IOException(MessageFormat.format( "Unsupported temporary file format (inconsistent version): file={0}, API={1}", version, MAJOR_VERSION)); } return BLOCK_HEADER.length + 1; } /** * Writes a string. * @param output the target output * @param string the contents * @return the bytes written * @throws IOException if failed to write */ public static int writeString(OutputStream output, String string) throws IOException { byte[] bytes = string.getBytes(ENCODING); int length = bytes.length; output.write((length >> 24) & 0xff); output.write((length >> 16) & 0xff); output.write((length >> 8) & 0xff); output.write((length >> 0) & 0xff); output.write(bytes); return 4 + bytes.length; } /** * Reads a string into appendable. * @param input the target input * @param appendable the target appendable * @return the bytes read * @throws IOException if failed to read */ public static int readString(InputStream input, Appendable appendable) throws IOException { int length = 0; for (int i = 0; i < 4; i++) { int c = input.read(); if (c < 0) { return -1; } length = length << 8 | c; } byte[] bytes = new byte[length]; IOUtils.readFully(input, bytes, 0, length); appendable.append(new String(bytes, ENCODING)); return 4 + bytes.length; } /** * Returns whether clients can write a content page into the current block. * @param positionInBlock the byte position in the current block * @param length the content length in bytes * @return {@code true} if clients can write a content page into the current block, otherwise {@code false} */ public static boolean canWritePage(int positionInBlock, int length) { int blockRest = BLOCK_SIZE - positionInBlock; // page-header + page-contents + next-page-header return length + (PAGE_HEADER_SIZE * 2) <= blockRest; } /** * Writes a content page header. * @param output the target output stream * @param length the content length * @throws IOException if failed to write a page header by I/O error */ public static void writeContentPageMark(OutputStream output, int length) throws IOException { if (length <= 0 || length > MAX_PAGE_SIZE) { throw new IllegalArgumentException(MessageFormat.format( "The content page is too large: {1} (> {0})", MAX_PAGE_SIZE, length)); } output.write(getHeader(length)); } /** * Writes an end-of-block header. * @param output the target output stream * @throws IOException if failed to write a page header by I/O error */ public static void writeEndOfBlockMark(OutputStream output) throws IOException { output.write(getHeader(PAGE_HEADER_EOB)); } private static byte[] getHeader(int value) { byte[] header = HEADER_BUFFER.get(); header[0] = (byte) ((value >> 16) & 0xff); header[1] = (byte) ((value >> 8) & 0xff); header[2] = (byte) ((value >> 0) & 0xff); return header; } /** * Reads a page header from the head of the stream. * @param input the target input stream * @return the page header * @throws IOException if failed to read a page header * @see #MAX_PAGE_SIZE * @see #PAGE_HEADER_EOB * @see #PAGE_HEADER_EOF */ public static int readPageHeader(InputStream input) throws IOException { byte[] header = HEADER_BUFFER.get(); int offset = 0; while (offset < header.length) { int read = input.read(header, offset, header.length - offset); if (read < 0) { return PAGE_HEADER_EOF; } offset += read; } int value = 0; value |= (header[0] & 0xff) << 16; value |= (header[1] & 0xff) << 8; value |= (header[2] & 0xff) << 0; return value; } private TemporaryFile() { return; } }