net.pms.newgui.components.WindowProperties.java Source code

Java tutorial

Introduction

Here is the source code for net.pms.newgui.components.WindowProperties.java

Source

/*
 * Digital Media Server, for streaming digital media to UPnP AV or DLNA
 * compatible devices based on PS3 Media Server and Universal Media Server.
 * Copyright (C) 2016 Digital Media Server developers.
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see http://www.gnu.org/licenses/.
 */
package net.pms.newgui.components;

import static org.apache.commons.lang3.StringUtils.isBlank;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class keeps track of desktop screens, their resolution and insets and
 * the size and position of the specified {@link Window}. The properties is
 * saved to and loaded from disk by a {@link WindowPropertiesConfiguration}
 * instance, so that the properties are maintained across restarts.
 * <p>
 * Listeners are used to track changes, and the linked
 * {@link WindowPropertiesConfiguration} is updated continuously. The specified
 * {@link Window} is initialized according to the
 * {@link WindowPropertiesConfiguration} instance and the constructor parameters
 * in the constructor.
 *
 * @author Nadahar
 */
@NotThreadSafe
public class WindowProperties implements WindowListener, ComponentListener {
    private String graphicsDevice;
    private Rectangle screenBounds;
    private Rectangle effectiveScreenBounds;
    private Insets screenInsets;
    private Rectangle windowBounds;
    private byte windowState;
    private Window window;
    private final WindowPropertiesConfiguration windowConfiguration;
    private final Dimension minimumSize;

    /**
     * Creates a new instance using the specified parameters.
     *
     * @param window the {@link Window} whose properties to keep track of.
     * @param standardSize the standard size of {@code window}.
     * @param mimimumSize the minimum size of {@code window}.
     * @param windowConfiguration the {@link WindowPropertiesConfiguration}
     *            instance for reading and writing the window properties.
     */
    public WindowProperties(@Nonnull Window window, @Nullable Dimension standardSize,
            @Nullable Dimension mimimumSize, @Nullable WindowPropertiesConfiguration windowConfiguration) {
        if (window == null) {
            throw new IllegalArgumentException("window cannot be null");
        }
        this.window = window;
        this.minimumSize = mimimumSize;
        if (mimimumSize != null) {
            window.setMinimumSize(mimimumSize);
        }
        this.windowConfiguration = windowConfiguration;
        if (windowConfiguration != null) {
            getConfiguration();
            GraphicsConfiguration windowGraphicsConfiguration = window.getGraphicsConfiguration();
            if (windowBounds != null && effectiveScreenBounds != null && graphicsDevice != null
                    && graphicsDevice.equals(windowGraphicsConfiguration.getDevice().getIDstring())
                    && screenBounds != null && screenBounds.equals(windowGraphicsConfiguration.getBounds())) {
                setWindowBounds();
            } else {
                Rectangle screen = effectiveScreenBounds != null ? effectiveScreenBounds
                        : windowGraphicsConfiguration.getBounds();
                if (standardSize != null && screen.width >= standardSize.width
                        && screen.height >= standardSize.height) {
                    window.setSize(standardSize);
                } else if (mimimumSize != null && (window.getWidth() < mimimumSize.width
                        || window.getHeight() < mimimumSize.getHeight())) {
                    window.setSize(mimimumSize);
                }
                window.setLocationByPlatform(true);
            }
            if (window instanceof Frame) {
                // Set maximized state
                int maximizedState = windowState & Frame.MAXIMIZED_BOTH;
                if (maximizedState != 0) {
                    ((Frame) window).setExtendedState(((Frame) window).getExtendedState() | maximizedState);
                }
            }
        }
        window.addWindowListener(this);
        window.addComponentListener(this);
    }

    /**
     * Unregisters the listeners and releases the {@link Window} set up by the
     * constructor. This instance will never be GC'ed unless this method is
     * called first, since the registered listeners has references to this
     * instance.
     */
    public void dispose() {
        // Unregister listeners
        if (window != null) {
            window.removeWindowListener(this);
            window.removeComponentListener(this);
            window = null;
        }
    }

    private boolean getConfiguration() {
        if (windowConfiguration == null) {
            return false;
        }
        boolean changed = false;
        if (graphicsDevice == null && windowConfiguration.graphicsDevice != null
                || graphicsDevice != null && !graphicsDevice.equals(windowConfiguration.graphicsDevice)) {
            graphicsDevice = windowConfiguration.graphicsDevice;
            changed = true;
        }
        if (screenBounds == null && windowConfiguration.screenBounds != null
                || screenBounds != null && !screenBounds.equals(windowConfiguration.screenBounds)) {
            screenBounds = windowConfiguration.screenBounds;
            changed = true;
        }
        if (screenInsets == null && windowConfiguration.screenInsets != null
                || screenInsets != null && !screenInsets.equals(windowConfiguration.screenInsets)) {
            screenInsets = windowConfiguration.screenInsets;
            changed = true;
        }
        if (windowState != windowConfiguration.windowState) {
            windowState = windowConfiguration.windowState;
            changed = true;
        }
        if (windowBounds == null && windowConfiguration.windowBounds != null
                || windowBounds != null && !windowBounds.equals(windowConfiguration.windowBounds)) {
            windowBounds = windowConfiguration.windowBounds;
            changed = true;
        }
        if (changed) {
            updateEffectiveScreenBounds();
        }
        return changed;
    }

    private void setConfiguration() {
        if (windowConfiguration == null) {
            return;
        }
        boolean changed = false;
        if (graphicsDevice == null && windowConfiguration.graphicsDevice != null
                || graphicsDevice != null && !graphicsDevice.equals(windowConfiguration.graphicsDevice)) {
            windowConfiguration.graphicsDevice = graphicsDevice;
            changed = true;
        }
        if (screenBounds == null && windowConfiguration.screenBounds != null
                || screenBounds != null && !screenBounds.equals(windowConfiguration.screenBounds)) {
            windowConfiguration.screenBounds = screenBounds;
            changed = true;
        }
        if (screenInsets == null && windowConfiguration.screenInsets != null
                || screenInsets != null && !screenInsets.equals(windowConfiguration.screenInsets)) {
            windowConfiguration.screenInsets = screenInsets;
            changed = true;
        }
        if (windowState != windowConfiguration.windowState) {
            windowConfiguration.windowState = windowState;
            changed = true;
        }
        if (windowBounds == null && windowConfiguration.windowBounds != null
                || windowBounds != null && !windowBounds.equals(windowConfiguration.windowBounds)) {
            windowConfiguration.windowBounds = windowBounds;
            changed = true;
        }
        if (changed) {
            windowConfiguration.writeConfiguration();
        }
    }

    private boolean updateDevice(@Nullable Window eventWindow) {
        if (eventWindow != window) {
            return false;
        }
        String deviceString = window.getGraphicsConfiguration().getDevice().getIDstring();
        if (deviceString == null) {
            if (graphicsDevice == null) {
                return false;
            }
        } else if (deviceString.equals(graphicsDevice)) {
            return false;
        }
        graphicsDevice = deviceString;
        return true;
    }

    private boolean updateEffectiveScreenBounds() {
        if (screenBounds == null) {
            return false;
        }
        Insets tmpInsets = screenInsets == null ? new Insets(0, 0, 0, 0) : screenInsets;
        Rectangle newEffectiveScreenBounds = new Rectangle(screenBounds.x, screenBounds.y,
                screenBounds.width - tmpInsets.left - tmpInsets.right,
                screenBounds.height - tmpInsets.top - tmpInsets.bottom);
        if (!newEffectiveScreenBounds.equals(effectiveScreenBounds)) {
            effectiveScreenBounds = newEffectiveScreenBounds;
            return true;
        }
        return false;
    }

    private boolean updateProperties(@Nullable Window eventWindow) {
        if (eventWindow != window) {
            return false;
        }
        boolean changed = updateWindowBounds(eventWindow);
        changed |= updateScreenProperties(eventWindow);
        changed |= updateDevice(eventWindow);
        if (changed && windowConfiguration != null) {
            setConfiguration();
        }
        return changed;
    }

    private void setWindowBounds() {
        if (windowBounds == null) {
            return;
        }
        if (effectiveScreenBounds == null) {
            window.setBounds(windowBounds);
            return;
        }
        int deltaX = 0;
        if (windowBounds.x + windowBounds.width > effectiveScreenBounds.x + effectiveScreenBounds.width) {
            deltaX = effectiveScreenBounds.x + effectiveScreenBounds.width - windowBounds.x - windowBounds.width;
        }
        if (windowBounds.x < effectiveScreenBounds.x) {
            deltaX = effectiveScreenBounds.x - windowBounds.x;
        }
        int deltaY = 0;
        if (windowBounds.y + windowBounds.height > effectiveScreenBounds.y + effectiveScreenBounds.height) {
            deltaY = effectiveScreenBounds.y + effectiveScreenBounds.height - windowBounds.y - windowBounds.height;
        }
        if (windowBounds.y < effectiveScreenBounds.y) {
            deltaY = effectiveScreenBounds.y - windowBounds.y;
        }
        if (deltaX != 0 || deltaY != 0) {
            windowBounds.translate(deltaX, deltaY);
        }
        if (!effectiveScreenBounds.contains(windowBounds)) {
            Rectangle newWindowBounds = windowBounds.intersection(effectiveScreenBounds);
            if (newWindowBounds.width < minimumSize.width) {
                newWindowBounds.width = minimumSize.width;
            }
            if (newWindowBounds.height < minimumSize.height) {
                newWindowBounds.height = minimumSize.height;
            }
            windowBounds = newWindowBounds;
        }
        window.setBounds(windowBounds);
    }

    private boolean updateScreenProperties(@Nullable Window eventWindow) {
        if (eventWindow != window) {
            return false;
        }
        boolean changed = updateScreenBounds(eventWindow);
        changed |= updateScreenInsets(eventWindow);
        if (changed && updateEffectiveScreenBounds()) {
            if (windowBounds != null && effectiveScreenBounds != null
                    && !effectiveScreenBounds.contains(windowBounds)) {
                setWindowBounds();
            }
        }
        return changed;
    }

    private boolean updateScreenBounds(@Nonnull Window eventWindow) {
        Rectangle bounds = eventWindow.getGraphicsConfiguration().getBounds();
        if (bounds == null) {
            if (screenBounds == null) {
                return false;
            }
        } else if (bounds.equals(screenBounds)) {
            return false;
        }
        screenBounds = bounds;
        return true;
    }

    private boolean updateScreenInsets(@Nonnull Window eventWindow) {
        Insets insets = eventWindow.getToolkit().getScreenInsets(eventWindow.getGraphicsConfiguration());
        if (insets == null) {
            if (screenInsets == null) {
                return false;
            }
        } else if (insets.equals(screenInsets)) {
            return false;
        }
        screenInsets = insets;
        return true;
    }

    private boolean updateWindowBounds(@Nullable Window eventWindow) {
        if (eventWindow != window) {
            return false;
        }
        int state = eventWindow instanceof Frame ? ((Frame) eventWindow).getExtendedState() : 0;
        Rectangle bounds;
        if (state == 0) {
            bounds = eventWindow.getBounds();
        } else if ((state & Frame.MAXIMIZED_BOTH) != Frame.MAXIMIZED_BOTH) {
            bounds = eventWindow.getBounds();
            // Don't store maximized dimensions
            if ((state & Frame.MAXIMIZED_HORIZ) != 0) {
                bounds.x = windowBounds.x;
                bounds.width = windowBounds.width;
            } else if ((state & Frame.MAXIMIZED_VERT) != 0) {
                bounds.y = windowBounds.y;
                bounds.height = windowBounds.height;
            }
        } else {
            bounds = windowBounds;
        }
        boolean changed = !bounds.equals(windowBounds);
        if (changed) {
            windowBounds = bounds;
        }
        if (windowState != (byte) state) {
            windowState = (byte) state;
            changed = true;
        }
        return changed;
    }

    @Override
    public void windowOpened(WindowEvent e) {
        updateProperties(e.getWindow());
    }

    @Override
    public void windowClosing(WindowEvent e) {
    }

    @Override
    public void windowClosed(WindowEvent e) {
    }

    @Override
    public void windowIconified(WindowEvent e) {
    }

    @Override
    public void windowDeiconified(WindowEvent e) {
    }

    @Override
    public void windowActivated(WindowEvent e) {
    }

    @Override
    public void windowDeactivated(WindowEvent e) {
    }

    @Override
    public void componentResized(ComponentEvent e) {
        updateProperties((Window) e.getSource());
    }

    @Override
    public void componentMoved(ComponentEvent e) {
        updateProperties((Window) e.getSource());
    }

    @Override
    public void componentShown(ComponentEvent e) {
    }

    @Override
    public void componentHidden(ComponentEvent e) {
    }

    /**
     * This class handles storage of window properties for a
     * {@link WindowProperties} instance.
     *
     * @author Nadahar
     */
    @NotThreadSafe
    public static class WindowPropertiesConfiguration {

        private static final Logger LOGGER = LoggerFactory.getLogger(WindowPropertiesConfiguration.class);

        private static final byte[] MAGIC_BYTES = { (byte) 68, (byte) 71, (byte) 83 };
        private static final byte VERSION = 1;
        private static final int BUFFER_SIZE = 128;
        private static final ByteOrder BYTE_ORDER = ByteOrder.BIG_ENDIAN;
        private static final Charset CHARSET = StandardCharsets.US_ASCII;
        private static final byte NULL = (byte) (1 << 7);

        private String graphicsDevice;
        private Rectangle screenBounds;
        private Insets screenInsets;
        private Rectangle windowBounds;
        private byte windowState;
        private Path path;

        /**
         * Creates a new instance bound to the specified {@link Path}.
         *
         * @param path the {@link Path} used to read and write window
         *            properties.
         */
        public WindowPropertiesConfiguration(@Nonnull Path path) {
            if (path == null) {
                throw new IllegalArgumentException("path cannot be null");
            }
            this.path = path;
            readConfiguration();
        }

        /**
         * Finds the {@link GraphicsConfiguration} that matches the previously
         * stored information, if any.
         * <p>
         * <b>Note:</b> This class is <i>not</i> thread-safe, and all other
         * methods is called either by the constructor or the event dispatcher
         * thread. Great care should be taken when calling this method to make
         * sure this {@link WindowPropertiesConfiguration} isn't currently in
         * use by a {@link WindowProperties} instance at the time.
         *
         * @return The matching {@link GraphicsConfiguration} or {@code null} if
         *         no match was found.
         */
        @Nullable
        public GraphicsConfiguration getGraphicsConfiguration() {
            if (isBlank(graphicsDevice) || screenBounds == null) {
                LOGGER.debug("No stored graphics device, using the default");
                return null;
            }
            for (GraphicsDevice graphicsDeviceItem : GraphicsEnvironment.getLocalGraphicsEnvironment()
                    .getScreenDevices()) {
                if (graphicsDevice.equals(graphicsDeviceItem.getIDstring())) {
                    for (GraphicsConfiguration graphicsConfiguration : graphicsDeviceItem.getConfigurations()) {
                        if (screenBounds.equals(graphicsConfiguration.getBounds())) {
                            return graphicsConfiguration;
                        }
                    }
                }
            }
            LOGGER.debug("No matching graphics configuration found, using the default");
            return null;
        }

        void readConfiguration() {
            ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
            try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
                boolean eof = fillBuffer(buffer, channel, 4);
                buffer.flip();
                if (buffer.remaining() < 4) {
                    LOGGER.warn("Invalid window properties configuration file \"{}\", file too short ({} bytes)",
                            path, buffer.remaining());
                    return;
                }
                byte[] bytes = new byte[3];
                buffer.get(bytes);
                if (!Arrays.equals(bytes, MAGIC_BYTES)) {
                    LOGGER.warn("Invalid window properties configuration file header in \"{}\"", path);
                    return;
                }
                if (buffer.get() != VERSION) {
                    LOGGER.debug("Incorrect window properties configuration version, ignoring file \"{}\"", path);
                    return;
                }
                if (eof && !buffer.hasRemaining()) {
                    LOGGER.warn("Empty window properties configuration, ignoring file \"{}\"", path);
                    return;
                }
                ReadResult<Rectangle> rectangle = readRectangle(buffer, channel);
                if (rectangle.error != null) {
                    LOGGER.warn("Windows properties configuration file \"{}\" error: {}", path, rectangle.error);
                    return;
                }
                screenBounds = rectangle.value;
                eof |= rectangle.eof;
                if (!buffer.hasRemaining()) {
                    LOGGER.warn("Window properties configuration file \"{}\" is truncated", path);
                    return;
                }
                ReadResult<Insets> insets = readInsets(buffer, channel);
                if (insets.error != null) {
                    LOGGER.warn("Windows properties configuration file \"{}\" error: {}", path, rectangle.error);
                    return;
                }
                screenInsets = insets.value;
                eof |= insets.eof;
                if (eof && !buffer.hasRemaining()) {
                    LOGGER.warn("Window properties configuration file \"{}\" is truncated", path);
                    return;
                }
                rectangle = readRectangle(buffer, channel);
                if (rectangle.error != null) {
                    LOGGER.warn("Windows properties configuration file \"{}\" error: {}", path, rectangle.error);
                    return;
                }
                windowBounds = rectangle.value;
                windowState = (byte) (rectangle.flags & ~NULL);
                eof |= rectangle.eof;
                if (eof && !buffer.hasRemaining()) {
                    LOGGER.warn("Window properties configuration file \"{}\" is truncated", path);
                    return;
                }
                ReadResult<String> string = readString(buffer, channel);
                if (string.error != null) {
                    LOGGER.warn("Windows properties configuration file \"{}\" error: {}", path, rectangle.error);
                    return;
                }
                graphicsDevice = string.value;
                eof |= string.eof;
                if (buffer.hasRemaining()) {
                    LOGGER.warn("Window properties configuration file \"{}\" contains unknown additional data",
                            path);
                    return;
                }
            } catch (IOException e) {
                if (e instanceof NoSuchFileException) {
                    LOGGER.debug("Window properties configuration file \"{}\" not found", path);
                } else {
                    LOGGER.error("Error reading window properties configuration file \"{}\": {}", path,
                            e.getMessage());
                    LOGGER.trace("", e);
                }
            }
        }

        void writeConfiguration() {
            try (ByteArrayOutputStream bytes = new ByteArrayOutputStream(BUFFER_SIZE)) {
                bytes.write(MAGIC_BYTES);
                bytes.write(VERSION);
                writeRectangle(bytes, screenBounds, (byte) 0);
                writeInsets(bytes, screenInsets);
                writeRectangle(bytes, windowBounds, windowState);
                writeString(bytes, graphicsDevice);
                try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.WRITE,
                        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
                    ByteBuffer buffer = ByteBuffer.wrap(bytes.toByteArray());
                    while (buffer.hasRemaining()) {
                        channel.write(buffer);
                    }
                }
            } catch (IOException e) {
                LOGGER.error("Error writing window properties configuration file \"{}\": {}", path, e.getMessage());
                LOGGER.trace("", e);
            }
        }

        private static boolean fillBuffer(ByteBuffer buffer, SeekableByteChannel channel, int count)
                throws IOException {
            int targetPosition = buffer.position() + count;
            if (targetPosition > buffer.capacity()) {
                throw new IndexOutOfBoundsException(
                        "Can't read " + count + " bytes into a free space of " + buffer.remaining() + "bytes");
            }
            boolean eof = false;
            while (!eof && buffer.position() < targetPosition) {
                eof = channel.read(buffer) < 0;
            }
            return eof;
        }

        private static ReadResult<String> readString(@Nonnull ByteBuffer buffer,
                @Nonnull SeekableByteChannel channel) throws IOException {
            boolean terminated = false;
            boolean eof = false;
            ReadResult<Byte> status = readByte(buffer, channel);
            eof |= status.eof;
            if (status.error != null || status.value == null) {
                return new ReadResult<>(null, eof, NULL, false, "Couldn't read string: " + status.error);
            }
            if ((status.value.byteValue() & NULL) != 0) {
                return new ReadResult<>(null, eof, status.value.byteValue(), false, null);
            }

            StringBuilder string = new StringBuilder();
            while (!terminated && !eof) {
                if (!buffer.hasRemaining()) {
                    buffer.clear();
                    eof = fillBuffer(buffer, channel, buffer.capacity());
                    buffer.flip();
                }
                if (buffer.hasRemaining()) {
                    int terminator = -1;
                    for (int i = buffer.position(); i < buffer.limit(); i++) {
                        if (buffer.get(i) == (byte) 0) {
                            terminator = i;
                            break;
                        }
                    }
                    byte[] bytes;
                    if (terminator < 0) {
                        bytes = new byte[buffer.remaining()];
                    } else {
                        terminated = true;
                        bytes = new byte[terminator - buffer.position()];
                    }
                    if (bytes.length > 0) {
                        buffer.get(bytes);
                        string.append(new String(bytes, CHARSET));
                    }
                }
            }
            if (buffer.hasRemaining()) {
                if (buffer.get() != (byte) 0) {
                    throw new AssertionError("Buffer has more data that doesn't start with the terminator");
                }
            }
            if (!terminated) {
                return new ReadResult<>(string.toString(), eof, status.value.byteValue(), true,
                        "String is truncated");
            }
            return new ReadResult<>(string.toString(), eof, status.value.byteValue());
        }

        private static ReadResult<Byte> readByte(ByteBuffer buffer, SeekableByteChannel channel)
                throws IOException {
            boolean eof = false;
            if (!buffer.hasRemaining()) {
                buffer.clear();
                eof = fillBuffer(buffer, channel, 1);
                buffer.flip();
            }
            if (buffer.hasRemaining()) {
                return new ReadResult<>(Byte.valueOf(buffer.get()), eof, (byte) 0);
            }
            return new ReadResult<>(null, eof, NULL, false, "Invalid file format or truncated file");
        }

        private static ReadResult<Integer> readInteger(ByteBuffer buffer, SeekableByteChannel channel)
                throws IOException {
            boolean eof = false;
            if (buffer.remaining() < 4) {
                buffer.compact();
                eof = fillBuffer(buffer, channel, 4);
                buffer.flip();
            }
            if (buffer.remaining() < 4) {
                return new ReadResult<>(null, eof, NULL, false, "File is truncated");
            }
            return new ReadResult<>(Integer.valueOf(buffer.getInt()), eof, (byte) 0);
        }

        private static ReadResult<Rectangle> readRectangle(ByteBuffer buffer, SeekableByteChannel channel)
                throws IOException {
            ReadResult<Byte> status = readByte(buffer, channel);
            boolean eof = status.eof;
            if (status.error != null || status.value == null) {
                return new ReadResult<>(null, eof, NULL, false, "Couldn't read rectangle: " + status.error);
            }
            if ((status.value.byteValue() & NULL) != 0) {
                return new ReadResult<>(null, eof, status.value.byteValue(), false, null);
            }

            ReadResult<Integer> integer = readInteger(buffer, channel);
            eof |= integer.eof;
            if (integer.value == null || integer.error != null) {
                return new ReadResult<>(null, eof, NULL, false, "Couldn't read rectangle: " + integer.error);
            }
            int x = integer.value.intValue();
            integer = readInteger(buffer, channel);
            eof |= integer.eof;
            if (integer.value == null || integer.error != null) {
                return new ReadResult<>(null, eof, NULL, false, "Couldn't read rectangle: " + integer.error);
            }
            int y = integer.value.intValue();
            integer = readInteger(buffer, channel);
            eof |= integer.eof;
            if (integer.value == null || integer.error != null) {
                return new ReadResult<>(null, eof, NULL, false, "Couldn't read rectangle: " + integer.error);
            }
            int width = integer.value.intValue();
            integer = readInteger(buffer, channel);
            eof |= integer.eof;
            if (integer.value == null || integer.error != null) {
                return new ReadResult<>(null, eof, NULL, false, "Couldn't read rectangle: " + integer.error);
            }
            int height = integer.value.intValue();
            return new ReadResult<>(new Rectangle(x, y, width, height), eof, status.value.byteValue());
        }

        private static ReadResult<Insets> readInsets(ByteBuffer buffer, SeekableByteChannel channel)
                throws IOException {
            ReadResult<Byte> status = readByte(buffer, channel);
            boolean eof = status.eof;
            if (status.error != null || status.value == null) {
                return new ReadResult<>(null, eof, NULL, false, "Couldn't read insets: " + status.error);
            }
            if ((status.value.byteValue() & NULL) != 0) {
                return new ReadResult<>(null, eof, status.value.byteValue(), false, null);
            }

            ReadResult<Integer> integer = readInteger(buffer, channel);
            eof |= integer.eof;
            if (integer.value == null || integer.error != null) {
                return new ReadResult<>(null, eof, NULL, false, "Couldn't read insets: " + integer.error);
            }
            int top = integer.value.intValue();
            integer = readInteger(buffer, channel);
            eof |= integer.eof;
            if (integer.value == null || integer.error != null) {
                return new ReadResult<>(null, eof, NULL, false, "Couldn't read insets: " + integer.error);
            }
            int left = integer.value.intValue();
            integer = readInteger(buffer, channel);
            eof |= integer.eof;
            if (integer.value == null || integer.error != null) {
                return new ReadResult<>(null, eof, NULL, false, "Couldn't read insets: " + integer.error);
            }
            int bottom = integer.value.intValue();
            integer = readInteger(buffer, channel);
            eof |= integer.eof;
            if (integer.value == null || integer.error != null) {
                return new ReadResult<>(null, eof, NULL, false, "Couldn't read insets: " + integer.error);
            }
            int right = integer.value.intValue();
            return new ReadResult<>(new Insets(top, left, bottom, right), eof, status.value.byteValue());
        }

        private static void writeRectangle(@Nonnull ByteArrayOutputStream bytes, @Nullable Rectangle rectangle,
                byte flags) throws IOException {
            if (rectangle == null) {
                bytes.write(flags | NULL);
                return;
            }

            ByteBuffer buffer = ByteBuffer.allocate(17);
            buffer.order(BYTE_ORDER);
            buffer.put(flags);
            buffer.putInt(rectangle.x);
            buffer.putInt(rectangle.y);
            buffer.putInt(rectangle.width);
            buffer.putInt(rectangle.height);
            bytes.write(buffer.array());
        }

        private static void writeInsets(@Nonnull ByteArrayOutputStream bytes, @Nullable Insets insets)
                throws IOException {
            if (insets == null) {
                bytes.write(NULL);
                return;
            }
            ByteBuffer buffer = ByteBuffer.allocate(17);
            buffer.order(BYTE_ORDER);
            buffer.put((byte) 0);
            buffer.putInt(insets.top);
            buffer.putInt(insets.left);
            buffer.putInt(insets.bottom);
            buffer.putInt(insets.right);
            bytes.write(buffer.array());
        }

        private static void writeString(@Nonnull ByteArrayOutputStream bytes, @Nullable String string)
                throws IOException {
            if (string == null) {
                bytes.write(NULL);
                return;
            }
            bytes.write((byte) 0);
            bytes.write(string.getBytes(CHARSET));
            bytes.write(0);
        }

        @SuppressWarnings("unused")
        private static class ReadResult<T> {
            private final T value;
            private final boolean eof;
            private final byte flags;
            private final boolean partial;
            private final String error;

            public ReadResult(T value, boolean eof, byte flags) {
                this.value = value;
                this.eof = eof;
                this.flags = flags;
                this.partial = false;
                this.error = null;
            }

            public ReadResult(T value, boolean eof, byte flags, boolean partial, String error) {
                this.value = value;
                this.eof = eof;
                this.flags = flags;
                this.partial = partial;
                this.error = error;
            }
        }

    }
}