/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * See LICENSE.txt included in this distribution for the specific * language governing permissions and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright (c) 2017, 2017, Oracle and/or its affiliates. All rights reserved. */ package org.opensolaris.opengrok.configuration.messages; import java.beans.IntrospectionException; import java.beans.PropertyDescriptor; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.opensolaris.opengrok.configuration.Configuration; import org.opensolaris.opengrok.configuration.RuntimeEnvironment; /** * * @author Vladimir Kotal * @author Krystof Tulinger */ public class ConfigMessage extends Message { /** * Pattern describes the java variable name and the assigned value. * Examples: * <ul> * <li>variable = true</li> * <li>stopOnClose = 10</li> * </ul> */ private static final Pattern VARIABLE_PATTERN = Pattern.compile("([a-z_]\\w*) = (.*)"); @Override protected byte[] applyMessage(RuntimeEnvironment env) throws IOException { if (hasTag("getconf")) { return env.getConfiguration().getXMLRepresentationAsString().getBytes(); } else if (hasTag("set")) { Matcher matcher = VARIABLE_PATTERN.matcher(getText()); if (matcher.find()) { // set the property invokeSetter( env.getConfiguration(), matcher.group(1), // field matcher.group(2) // value ); // apply the configuration - let the environment reload the configuration if necessary env.applyConfig(env.getConfiguration(), false); return String.format("Variable \"%s\" set to \"%s\".", matcher.group(1), matcher.group(2)).getBytes(); } else { // invalid pattern throw new IOException( String.format("The pattern \"%s\" does not match \"%s\".", VARIABLE_PATTERN.toString(), getText())); } } else if (hasTag("setconf")) { env.applyConfig(this, hasTag("reindex")); } return null; } /** * Invokes a setter on the configuration object and passes a value to that * setter. * * The value is passed as string and the function will automatically try to * convert it to the parameter type in the setter. These conversion are * available only for * <a href="https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html">java * primitive datatypes</a>: * <ul> * <li>Boolean or boolean</li> * <li>Short or short</li> * <li>Integer or integer</li> * <li>Long or long</li> * <li>Float or float</li> * <li>Double or double</li> * <li>Byte or byte</li> * <li>Character or char</li> * <li>String</li> * </ul> * Any other parameter type will cause an exception. * * @param config the configuration object * @param field name of the field which will be changed * @param value desired value * * @throws IOException if any error occurs (no suitable method, bad * conversion, ...) */ protected void invokeSetter(Configuration config, String field, String value) throws IOException { try { PropertyDescriptor desc = new PropertyDescriptor(field, Configuration.class); Method setter = desc.getWriteMethod(); if (setter == null) { // no setter throw new IOException( String.format("No setter for the name \"%s\".", field)); } if (setter.getParameterCount() != 1) { // not a setter /** * Actually should not happen as it is not considered as a * writer method so an exception would be thrown earlier. */ throw new IOException( String.format("The setter \"%s\" for the name \"%s\" does not take exactly 1 parameter.", setter.getName(), field)); } Class c = setter.getParameterTypes()[0]; String paramClass = c.getName(); /** * Java primitive types as per * <a href="https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html">java * datatypes</a>. */ if (paramClass.equals("boolean") || paramClass.equals(Boolean.class.getName())) { if (!isBoolean(value)) { throw new IOException(String.format("Unsupported type conversion from String to a boolean for name \"%s\" -" + " got \"%s\" - allowed values are [false, off, 0, true, on, 1].", field, value)); } Boolean v = Boolean.valueOf(value); if (!v) { /** * The Boolean.valueOf() returns true only for "true" case * insensitive so now we have either the false values or * "on" or "1". These are convenient shortcuts for "on", "1" * to be interpreted as booleans. */ v = v || value.equalsIgnoreCase("on"); v = v || value.equals("1"); } setter.invoke(config, v); } else if (paramClass.equals("short") || paramClass.equals(Short.class.getName())) { setter.invoke(config, Short.valueOf(value)); } else if (paramClass.equals("int") || paramClass.equals(Integer.class.getName())) { setter.invoke(config, Integer.valueOf(value)); } else if (paramClass.equals("long") || paramClass.equals(Long.class.getName())) { setter.invoke(config, Long.valueOf(value)); } else if (paramClass.equals("float") || paramClass.equals(Float.class.getName())) { setter.invoke(config, Float.valueOf(value)); } else if (paramClass.equals("double") || paramClass.equals(Double.class.getName())) { setter.invoke(config, Double.valueOf(value)); } else if (paramClass.equals("byte") || paramClass.equals(Byte.class.getName())) { setter.invoke(config, Byte.valueOf(value)); } else if (paramClass.equals("char") || paramClass.equals(Character.class.getName())) { setter.invoke(config, value.charAt(0)); } else if (paramClass.equals(String.class.getName())) { setter.invoke(config, value); } else { // error uknown type throw new IOException( String.format("Unsupported type conversion for the name \"%s\". Expecting \"%s\".", field, paramClass)); } } catch (NumberFormatException ex) { throw new IOException( String.format("Unsupported type conversion from String to a number for name \"%s\" - %s.", field, ex.getLocalizedMessage()), ex); } catch (IndexOutOfBoundsException ex) { throw new IOException( String.format("The string is not long enough to extract 1 character for name \"%s\" - %s.", field, ex.getLocalizedMessage()), ex); } catch (IntrospectionException | IllegalAccessException | IllegalArgumentException /** * This the case when the invocation failed because the invoked * method failed with an exception. All exceptions are * propagated through this exception. */ | InvocationTargetException ex) { throw new IOException( String.format("Unsupported operation with the configuration for name \"%s\" - %s.", field, ex.getCause() == null ? ex.getLocalizedMessage() : ex.getCause().getLocalizedMessage()), ex); } } /** * Validate the string if it contains a boolean value. * * <p> * Boolean values are (case insensitive): * <ul> * <li>false</li> * <li>off</li> * <li>0</li> * <li>true</li> * <li>on</li> * <li>1</li> * </ul> * </p> * * @param value the string value * @return if the value is boolean or not */ private boolean isBoolean(String value) { return "false".equalsIgnoreCase(value) || "off".equalsIgnoreCase(value) || "0".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value) || "1".equalsIgnoreCase(value); } /** * Cast a boolean value to integer. * * @param b boolean value * @return 0 for false and 1 for true */ protected int toInteger(boolean b) { return b ? 1 : 0; } @Override public void validate() throws Exception { if (toInteger(hasTag("setconf")) + toInteger(hasTag("getconf")) + toInteger(hasTag("set")) > 1) { throw new Exception("The message tag must be either setconf, getconf or set"); } if (hasTag("setconf")) { if (getText() == null) { throw new Exception("The setconf message must contain a text."); } } else if (hasTag("getconf")) { if (getText() != null) { throw new Exception("The getconf message should not contain a text."); } if (getTags().size() != 1) { throw new Exception("The getconf message should be the only tag."); } } else if (hasTag("set")) { if (getText() == null) { throw new Exception("The set message must contain a text."); } if (getTags().size() != 1) { throw new Exception("The set message should be the only tag."); } } else { throw new Exception("The message tag must be either setconf, getconf or set"); } super.validate(); } }