/* * Copyright 2015 Cel Skeggs * * This file is part of the CCRE, the Common Chicken Runtime Engine. * * The CCRE is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * The CCRE 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 Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with the CCRE. If not, see <http://www.gnu.org/licenses/>. */ package ccre.rconf; import java.io.Serializable; import ccre.util.Utils; import ccre.verifier.IgnoredPhase; import ccre.verifier.SetupPhase; /** * The RConf subsystem's utility class. * * The basic idea of RConf is best understood like a web browser or other client * - the client (usually the PoultryInspector) requests a snapshot of data from * an RConfable device, and then displays it to the user once received. * * The user can then interact with certain components of the result in certain * ways, and this sends data back to the original RConfable. (The RConfable, of * course, doesn't have to do anything with this data.) * * @author skeggsc */ public class RConf { /** * The type of a TITLE component. This has large text, and is used for * clearly describing the device. */ public static final byte F_TITLE = 0; /** * The type of a BOOLEAN component. This can be either true or false, and * the user can set it to true or false remotely if the RConfable wants. */ public static final byte F_BOOLEAN = 1; /** * The type of an INTEGER component. This can be any integer, and the user * can modify it remotely if the RConfable wants. */ public static final byte F_INTEGER = 2; /** * The type of a FLOAT component. This can be any float, and the user can * modify it remotely if the RConfable wants. */ public static final byte F_FLOAT = 3; /** * The type of a STRING component. This is displayed with normal-sized text, * and the user can modify it remotely if the RConfable wants. */ public static final byte F_STRING = 4; /** * The type of a BUTTON component. This is displayed with a border, and the * user can press it. */ public static final byte F_BUTTON = 5; /** * The type of a CLUCK reference. This allows for, essentially, a "link" to * another entry on the Cluck network. The path is relative to the * RConfable's publishing location. */ public static final byte F_CLUCK_REF = 6; /** * The type of an AUTOREFERENCE reference. This strongly implies to the * client that it should refresh every some number of milliseconds. */ public static final byte F_AUTO_REFRESH = 7; /** * An entry as part of the result from querying an RConfable. Could be any * of the types available statically on RConf - such as F_TITLE, F_BUTTON, * F_INTEGER, or others. * * Contains a type and a byte array of the associated data. * * @author skeggsc */ public static final class Entry implements Serializable { private static final long serialVersionUID = -5212798086046991238L; /** * The type of this entry. See the static fields on RConf. */ public final byte type; /** * The contents of this entry. The meaning depends on the type. */ public final byte[] contents; /** * Create a new RConf Entry with a type and some contents. * * @param type the type of the entry. * @param contents the byte array contents of the entry. */ public Entry(byte type, byte... contents) { this.type = type; this.contents = contents; } /** * Parse the contents of this Entry as a textual value. * * @return the string representing the contents. * @throws IllegalStateException if the type of this entry is not * supposed to contain text. */ @IgnoredPhase public String parseTextual() throws IllegalStateException { if (type != F_TITLE && type != F_STRING && type != F_BUTTON && type != F_CLUCK_REF) { throw new IllegalStateException("Invalid type of Entry in parseTextual: " + type); } return Utils.fromBytes(contents, 0, contents.length); } /** * Parse the contents of this Entry as a boolean value. * * @return the boolean representing the contents, or null if invalid. * @throws IllegalStateException if the type of this entry is not * supposed to contain a boolean. */ @IgnoredPhase public Boolean parseBoolean() { if (type != F_BOOLEAN) { throw new IllegalStateException("Invalid type of Entry in parseBoolean: " + type); } return contents.length >= 1 ? contents[0] != 0 : null; } /** * Parse the contents of this Entry as an integer value. * * @return the integer representing the contents, or null if invalid. * @throws IllegalStateException if the type of this entry is not * supposed to contain a integer. */ @IgnoredPhase public Integer parseInteger() { if (type != F_INTEGER && type != F_AUTO_REFRESH) { throw new IllegalStateException("Invalid type of Entry in parseInteger: " + type); } return getAsInteger(); } /** * Parse the contents of this Entry as a float value. * * @return the float representing the contents, or null if invalid. * @throws IllegalStateException if the type of this entry is not * supposed to contain a float. */ @IgnoredPhase public Float parseFloat() { if (type != F_FLOAT) { throw new IllegalStateException("Invalid type of Entry in parseFloat: " + type); } return Float.intBitsToFloat(getAsInteger()); } @IgnoredPhase private Integer getAsInteger() { return contents.length >= 4 ? (((contents[0] & 0xFF) << 24) | ((contents[1] & 0xFF) << 16) | ((contents[2] & 0xFF) << 8) | (contents[3] & 0xFF)) : null; } @Override public String toString() { switch (type) { case F_TITLE: String title = parseTextual(); return title == null ? "<invalid:bad-title>" : "# " + title; case F_BOOLEAN: Boolean b = parseBoolean(); return b == null ? "<invalid:bad-bool>" : b.toString(); case F_INTEGER: Integer i = parseInteger(); return i == null ? "<invalid:bad-int>" : i.toString(); case F_FLOAT: Float f = parseFloat(); return f == null ? "<invalid:bad-float>" : f.toString(); case F_STRING: String text = parseTextual(); return text == null ? "<invalid:bad-str>" : text; case F_BUTTON: String label = parseTextual(); return label == null ? "<invalid:bad-label>" : "[" + label + "]"; case F_CLUCK_REF: String ref = parseTextual(); return ref == null ? "<invalid:bad-cluck>" : "@" + ref; case F_AUTO_REFRESH: Integer timeout = parseInteger(); return timeout == null ? "<invalid:bad-auto-refresh>" : "<meta:auto-refresh:" + timeout + ">"; default: return "<invalid:bad-type>"; } } } /** * Create a new TITLE component with the given textual contents. * * @param title the title information. * @return the new RConf entry. */ @SetupPhase public static Entry title(String title) { return new Entry(F_TITLE, Utils.getBytes(title)); } /** * Create a new STRING component with the given textual contents. * * @param data the textual information. * @return the new RConf entry. */ @SetupPhase public static Entry string(String data) { return new Entry(F_STRING, Utils.getBytes(data)); } /** * Create a new BUTTON component with the given textual label. * * @param label the button label. * @return the new RConf entry. */ @SetupPhase public static Entry button(String label) { return new Entry(F_BUTTON, Utils.getBytes(label)); } /** * Create a new CLUCK REFERENCE component with the given Cluck path, * relative to where the RConfable in which this is used will be published. * * @param ref the relative path. * @return the new RConf entry. */ @SetupPhase public static Entry cluckRef(String ref) { return new Entry(F_CLUCK_REF, Utils.getBytes(ref)); } /** * Create a new INTEGER component with the given integer content. * * @param integer the integer content. * @return the new RConf entry. */ @SetupPhase public static Entry fieldInteger(int integer) { return new Entry(F_INTEGER, integerAsBytes(integer)); } /** * Create a new FLOAT component with the given float content. * * @param f the float content. * @return the new RConf entry. */ @SetupPhase public static Entry fieldFloat(float f) { return new Entry(F_FLOAT, integerAsBytes(Float.floatToIntBits(f))); } @IgnoredPhase private static byte[] integerAsBytes(int b) { return new byte[] { (byte) (b >> 24), (byte) (b >> 16), (byte) (b >> 8), (byte) b }; } /** * Create a new BOOLEAN component with the given boolean value. * * @param bool the boolean value. * @return the new RConf entry. */ @SetupPhase public static Entry fieldBoolean(boolean bool) { return new Entry(F_BOOLEAN, bool ? (byte) 1 : (byte) 0); } /** * Create a new AUTO_REFRESH component with the given boolean value. * * @param timeout the timeout, in milliseconds. * @return the new RConf entry. */ @SetupPhase public static Entry autoRefresh(int timeout) { return new Entry(F_AUTO_REFRESH, integerAsBytes(timeout)); } /** * A helper method to convert bytes into a float. * * @param data the four bytes of data to convert. * @return the resulting float. */ @IgnoredPhase public static float bytesToFloat(byte[] data) { return Float.intBitsToFloat(((data[0] & 0xFF) << 24) | ((data[1] & 0xFF) << 16) | ((data[2] & 0xFF) << 8) | (data[3] & 0xFF)); } private RConf() { } }