All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jvnet.hk2.config.ConfigSupport Maven / Gradle / Ivy

There is a newer version: 7.2024.1.Alpha1
Show newest version
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2007-2017 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://oss.oracle.com/licenses/CDDL+GPL-1.1
 * or LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
// Portions Copyright [2020] Payara Foundation and/or affiliates

package org.jvnet.hk2.config;

import org.glassfish.hk2.api.MultiException;
import org.glassfish.hk2.api.ProxyCtl;
import org.glassfish.hk2.api.ServiceLocator;
import org.jvnet.hk2.annotations.Service;
import org.jvnet.hk2.config.api.ConfigurationUtilities;

import javax.inject.Inject;

import java.beans.PropertyVetoException;
import java.beans.PropertyChangeEvent;
import java.lang.reflect.*;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.*;

import org.jvnet.tiger_types.Types;

/**
  * 

* Helper class to execute some code on configuration objects while taking * care of the transaction boiler plate code. *

*

* Programmers that wish to apply some changes to configuration objects * can use these convenience methods to reduce the complexity of handling * transactions. *

*

* For instance, say a programmer need to change the HttpListener port from * 8080 to 8989, it just needs to do : *

*
  *     ... in his code somewhere ...
  *     HttpListener httpListener = domain.get...
  *
  *     // If the programmer tries to modify the httpListener directly
  *     // it will get an exception
  *     httpListener.setPort("8989"); // will generate a PropertyVetoException
  *
  *     // instead he needs to use a transaction and can use the helper services
  *     ConfigSupport.apply(new SingleConfigCode() {
  *         public Object run(HttpListener okToChange) throws PropertyException {
  *             okToChange.setPort("8989"); // good...
  *             httpListener.setPort("7878"); // not good, exceptions still raised...
  *             return null;
  *         });
  *
  *     // Note that after this code
  *     System.out.println("Port is " + httpListener.getPort());
  *     // will display 8989
  * }
  * 
* @author Jerome Dochez */ @Service public class ConfigSupport implements ConfigurationUtilities { private static final Logger LOG = Logger.getLogger(ConfigSupport.class.getName()); public static int lockTimeOutInSeconds = Integer.getInteger("org.glassfish.hk2.config.locktimeout", 3); @Inject ServiceLocator habitat; /** * Execute some logic on one config bean of type T protected by a transaction * * @param code code to execute * @param param config object participating in the transaction * @return list of events that represents the modified config elements. * @throws TransactionFailure when code did not run successfully */ public static Object apply(final SingleConfigCode code, T param) throws TransactionFailure { //ConfigBeanProxy[] objects = { param }; return apply((new ConfigCode() { @Override @SuppressWarnings("unchecked") public Object run(ConfigBeanProxy... objects) throws PropertyVetoException, TransactionFailure { return code.run((T) objects[0]); } }), param); } /** * Executes some logic on some config beans protected by a transaction. * * @param code code to execute * @param objects config beans participating to the transaction * @return list of property change events * @throws TransactionFailure when the code did run successfully due to a * transaction exception */ public static Object apply(ConfigCode code, ConfigBeanProxy... objects) throws TransactionFailure { ConfigBean source = (ConfigBean) Dom.unwrap(objects[0]); return source.getHabitat().getService(ConfigSupport.class)._apply(code, objects); } /** * Executes some logic on some config beans protected by a transaction. * * @param code code to execute * @param objects config beans participating to the transaction * @return list of property change events * @throws TransactionFailure when the code did run successfully due to a * transaction exception */ public Object _apply(ConfigCode code, ConfigBeanProxy... objects) throws TransactionFailure { LOG.finest(() -> String.format("_apply(code=%s, objects=%s)", code, objects)); // the fools think they operate on the "real" object while I am // feeding them with writeable view. Only if the transaction succeed // will I apply the "changes" to the real ones. WriteableView[] views = new WriteableView[objects.length]; ConfigBeanProxy[] proxies = new ConfigBeanProxy[objects.length]; // create writeable views. for (int i=0;i WriteableView getWriteableView(T s, ConfigBean sourceBean) throws TransactionFailure { WriteableView f = new WriteableView(s); try { if (sourceBean.getLock().tryLock(lockTimeOutInSeconds, TimeUnit.SECONDS)) { sourceBean.setWriteableView(f); return f; } } catch(InterruptedException e) { // ignore, will throw a TransactionFailure exception } throw new TransactionFailure("Config bean already locked " + sourceBean, null); } /** * Returns a writeable view of a configuration object * @param source the configured interface implementation * @return the new interface implementation providing write access * @throws TransactionFailure if the object cannot be enrolled (probably already enrolled in * another transaction). */ public T getWriteableView(final T source) throws TransactionFailure { T configBeanProxy = revealProxy(source); ConfigView sourceBean = (ConfigView) Proxy.getInvocationHandler(configBeanProxy); WriteableView writeableView = getWriteableView(configBeanProxy, (ConfigBean) sourceBean.getMasterView()); return (T) writeableView.getProxy(sourceBean.getProxyType()); } /** * Return the main implementation bean for a proxy. * @param source configuration interface proxy * @return the implementation bean */ public static ConfigView getImpl(ConfigBeanProxy source) { Object bean = Proxy.getInvocationHandler(source); if (bean instanceof ConfigView) { return ((ConfigView) bean).getMasterView(); } else { return (ConfigBean) bean; } } /** * Returns the type of configuration object this config proxy represents. * @param element is the configuration object * @return the configuration interface class */ public static Class proxyType(T element) { ConfigView bean = getImpl(element); return bean.getProxyType(); } /** * sort events and dispatch the changes. There will be only one notification of event * per event type, per object, meaning that if an object has had 3 attributes changes, the * Changed interface implementation will get notified only once. * * @param events of events that resulted of a successful configuration transaction * @param target the intended receiver of the changes notification * @param logger to log any issues. */ public static UnprocessedChangeEvents sortAndDispatch(PropertyChangeEvent[] events, Changed target, Logger logger) { if (logger == null) { throw new IllegalArgumentException(); } List unprocessed = new ArrayList<>(); List added = new ArrayList<>(); List changed = new ArrayList<>(); for (PropertyChangeEvent event : events) { if (event.getOldValue()==null && event.getNewValue() instanceof ConfigBeanProxy) { // something was added try { final ConfigBeanProxy proxy = ConfigBeanProxy.class.cast(event.getNewValue()); added.add(Dom.unwrap(proxy)); final NotProcessed nc = target.changed(Changed.TYPE.ADD, proxyType(proxy), proxy); if ( nc != null ) { unprocessed.add( new UnprocessedChangeEvent(event, nc.getReason() ) ); } } catch (Exception e) { logger.log(Level.SEVERE, "Exception while processing config bean changes : ", e); } } } for (PropertyChangeEvent event : events) { try { Dom eventSource = Dom.unwrap((ConfigBeanProxy) event.getSource()); if (added.contains(eventSource)) { // we don't really send the changed events for new comers. continue; } ConfigBeanProxy proxy = null; if (event.getNewValue()==null) { try { // getOldValue() can be null, we will notify a CHANGE event proxy = ConfigBeanProxy.class.cast(event.getOldValue()); } catch (ClassCastException e) { // this is ok, the old value was probably a string or something like this... // we will notify the event.getSource() that it changed. } // new value is null, but not old value, we removed something if (proxy!=null) { final NotProcessed nc = target.changed(Changed.TYPE.REMOVE, proxyType(proxy), proxy ); if ( nc != null ) { unprocessed.add( new UnprocessedChangeEvent(event, nc.getReason() ) ); } continue; } } // we fall back in this case, the newValue was nullm the old value was also null or was not a ConfigBean, // we revert to just notify of the change. // when the new value is not null, we also send the CHANGE on the source all the time, unless this was // and added config bean. if (!changed.contains(eventSource)) { proxy = ConfigBeanProxy.class.cast(event.getSource()); changed.add(eventSource); final NotProcessed nc = target.changed(Changed.TYPE.CHANGE, proxyType(proxy), proxy); if ( nc != null ) { unprocessed.add( new UnprocessedChangeEvent(event, nc.getReason() ) ); } } } catch (Exception e) { logger.log(Level.SEVERE, "Exception while processing config bean changes : ", e); } } return new UnprocessedChangeEvents( unprocessed ); } // kind of insane, just to get the proper return type for my properties. static private List defaultPropertyValue() { return null; } public void apply(Map> mapOfChanges) throws TransactionFailure { Transaction t = new Transaction(); try { for (Map.Entry> configBeanChange : mapOfChanges.entrySet()) { ConfigBean source = configBeanChange.getKey(); ConfigBeanProxy readableView = source.getProxy(source.getProxyType()); WriteableView writeable = getWriteableView(readableView, source); if (!writeable.join(t)) { t.rollback(); throw new TransactionFailure("Cannot enlist " + source.getProxyType() + " in transaction", null); } for (Map.Entry change : configBeanChange.getValue().entrySet()) { String xmlName = change.getKey(); ConfigModel.Property prop = writeable.getProperty(xmlName); if (prop == null) { throw new TransactionFailure("Unknown property name " + xmlName + " on " + source.getProxyType(), null); } if (prop.isCollection()) { try { List values = (List) writeable.getter(prop, ConfigSupport.class.getDeclaredMethod("defaultPropertyValue", null).getGenericReturnType()); values.add(change.getValue()); } catch (NoSuchMethodException e) { throw new TransactionFailure("Unknown property name " + xmlName + " on " + source.getProxyType(), null); } } else { writeable.setter(prop, change.getValue(), String.class); } } } } catch(TransactionFailure e) { t.rollback(); throw e; } catch (Exception e) { t.rollback(); throw new TransactionFailure(e.getMessage(), e); } try { t.commit(); } catch (RetryableException e) { LOG.log(Level.WARNING, "Retryable...", e); // TODO : do something meaninful here t.rollback(); throw new TransactionFailure(e.getMessage(), e); } catch (TransactionFailure e) { LOG.log(Level.SEVERE, "failure, not retryable..."); t.rollback(); throw e; } } /** * Returns the list of sub-elements supported by a ConfigBean * @return array of classes reprensenting the sub elements of a particular * @throws ClassNotFoundException for severe errors with the model associated * with the passed config bean. */ public static Class[] getSubElementsTypes(ConfigBean bean) throws ClassNotFoundException { List> subTypes = new ArrayList<>(); for (ConfigModel.Property element : bean.model.elements.values()) { if (!element.isLeaf()) { ConfigModel elementModel = ((ConfigModel.Node) element).model; Class subType = elementModel.classLoaderHolder.loadClass(elementModel.targetTypeName); subTypes.add(subType); } else { if (element.isCollection()) { subTypes.add(List.class); } } } return subTypes.toArray(new Class[subTypes.size()]); } /** * Returns the list of attributes names by the passed ConfigBean * @return array of String for all the attributes names */ public String[] getAttributesNames(ConfigBean bean) { return xmlNames(bean.model.attributes.values()); } /** * Returns the list of elements names by the passed ConfigBean * @return array of String for all the elements names */ public String[] getElementsNames(ConfigBean bean) { return xmlNames(bean.model.elements.values()); } private String[] xmlNames(Collection properties) { List names = new ArrayList<>(); for (ConfigModel.Property attribute : properties) { names.add(attribute.xmlName()); } return names.toArray(new String[names.size()]); } /** * Creates a new child of the passed child and add it to the parent's live * list of elements. The child is also initialized with the attributes passed * where each key represent the xml property name for the attribute and the value * represent the attribute's value. * * This code will be executed within a Transaction and can therefore throw * a TransactionFailure when the creation or settings of attributes failed. * * Example creating a new http-listener element under http-service * ConfigBean httpService = ... // got it from somwhere. * Map attributes = new HashMap(); * attributes.put("id", "jerome-listener"); * attributes.put("enabled", "true"); * ConfigSupport.createAndSet(httpService, HttpListener.class, attributes); * * @param parent parent config bean to which the child will be added. * @param childType child type * @param attributes map of key value pair to set on the newly created child * @throws TransactionFailure if the creation or attribute settings failed */ public static ConfigBean createAndSet( final ConfigBean parent, final Class childType, final Map attributes, final TransactionCallBack runnable) throws TransactionFailure { return createAndSet(parent, childType, convertMapToAttributeChanges(attributes), runnable); } /** * Creates a new child of the passed child and add it to the parent's live * list of elements. The child is also initialized with the attributes passed * where each key represent the xml property name for the attribute and the value * represent the attribute's value. * * This code will be executed within a Transaction and can therefore throw * a TransactionFailure when the creation or settings of attributes failed. * * Example creating a new http-listener element under http-service * ConfigBean httpService = ... // got it from somwhere. * Map attributes = new HashMap(); * attributes.put("id", "jerome-listener"); * attributes.put("enabled", "true"); * ConfigSupport.createAndSet(httpService, HttpListener.class, attributes); * * @param parent parent config bean to which the child will be added. * @param childType child type * @param attributes list of attribute changes to apply to the newly created child * @param runnable code that will be invoked as part of the transaction to add * more attributes or elements to the newly create type * @throws TransactionFailure if the creation or attribute settings failed */ public static ConfigBean createAndSet( final ConfigBean parent, final Class childType, final List attributes, final TransactionCallBack runnable) throws TransactionFailure { return parent.getHabitat().getService(ConfigSupport.class). _createAndSet(parent, childType, attributes, runnable); } private ConfigBean _createAndSet( final ConfigBean parent, final Class childType, final List attributes, final TransactionCallBack runnable) throws TransactionFailure { LOG.finest(() -> String.format("_createAndSet(parent=%s, childType=%s, attributes=%s, runnable=%s)", parent, childType, attributes, runnable)); ConfigBeanProxy readableView = parent.getProxy(parent.getProxyType()); ConfigBeanProxy readableChild = (ConfigBeanProxy) apply(new SingleConfigCode() { @Override public Object run(ConfigBeanProxy param) throws PropertyVetoException, TransactionFailure { return addChildWithAttributes(param, parent, childType, attributes, runnable); } }, readableView); return (ConfigBean) Dom.unwrap(readableChild); } /** * Creates a new child of the passed child and add it to the parent's live * list of elements. The child is also initialized with the attributes passed * where each key represent the xml property name for the attribute and the value * represent the attribute's value. * * This code will be executed within a Transaction and can therefore throw * a TransactionFailure when the creation or settings of attributes failed. * * Example creating a new http-listener element under http-service * ConfigBean httpService = ... // got it from somwhere. * Map attributes = new HashMap(); * attributes.put("id", "jerome-listener"); * attributes.put("enabled", "true"); * ConfigSupport.createAndSet(httpService, HttpListener.class, attributes); * * @param parent parent config bean to which the child will be added. * @param childType child type * @param attributes list of attributes changes to apply to the new created child * @throws TransactionFailure if the creation or attribute settings failed */ public static ConfigBean createAndSet( final ConfigBean parent, final Class childType, final Map attributes) throws TransactionFailure { return createAndSet(parent, childType, attributes, null); } public ConfigBean createAndSet( final ConfigBean parent, final Class childType, final List attributes) throws TransactionFailure { return createAndSet(parent, childType, attributes, null); } public static void deleteChild( final ConfigBean parent, final ConfigBean child) throws TransactionFailure { ConfigBeanProxy readableView = parent.getProxy(parent.getProxyType()); apply(new SingleConfigCode() { @Override public Object run(ConfigBeanProxy param) throws PropertyVetoException, TransactionFailure { // remove the child from the parent. WriteableView writeableParent = (WriteableView) Proxy.getInvocationHandler(param); _deleteChild(parent, writeableParent, child); return child; } }, readableView); } /** * Unprotected child deletion, caller must start a transaction before calling this * method. * * @param parent the parent element * @param writeableParent the writeable view of the parent element * @param child the child to delete * @throws TransactionFailure if something goes wrong. */ public static void _deleteChild( final ConfigBean parent, final WriteableView writeableParent, final ConfigBean child) throws TransactionFailure { LOG.finest(() -> String.format("_deleteChild(parent=%s, writeableParent=%s, child=%s)", parent, writeableParent, child)); final Class childType = child.getProxyType(); // get the child ConfigBeanProxy childProxy = child.getProxy(childType); // get the parent type Class parentProxyType = parent.getProxyType(); // first we need to find the element associated with this type ConfigModel.Property element = null; for (ConfigModel.Property e : parent.model.elements.values()) { if (!(e instanceof ConfigModel.Node)) { continue; } ConfigModel elementModel = ((ConfigModel.Node) e).model; try { final Class targetClass = parent.model.classLoaderHolder.loadClass(elementModel.targetTypeName); if (targetClass.isAssignableFrom(childType)) { element = e; break; } } catch(Exception ex) { // ok. } } // now depending whether this is a collection or a single leaf, // we need to process this setting differently if (element != null) { if (element.isCollection()) { // this is kind of nasty, I have to find the method that returns the collection // object because trying to do a element.get without having the parametized List // type will not work. for (Method m : parentProxyType.getMethods()) { final Class returnType = m.getReturnType(); if (Collection.class.isAssignableFrom(returnType) && (m.getParameterTypes().length == 0)) { // this could be it... if (!(m.getGenericReturnType() instanceof ParameterizedType)) { throw new IllegalArgumentException("List needs to be parameterized"); } final Class itemType = Types.erasure(Types.getTypeArgument(m.getGenericReturnType(), 0)); if (itemType.isAssignableFrom(childType)) { List list = null; try { list = (List) m.invoke(writeableParent.getProxy(writeableParent.getProxyType()), null); } catch (IllegalAccessException e) { throw new TransactionFailure("Exception while adding to the parent", e); } catch (InvocationTargetException e) { throw new TransactionFailure("Exception while adding to the parent", e); } if (list != null) { list.remove(childProxy); break; } } } } } else { // much simpler, I can use the setter directly. writeableParent.setter(element, null, childType); } } else { throw new TransactionFailure("Parent " + parent.getProxyType() + " does not have a child of type " + childType); } } public static List convertMapToAttributeChanges(Map values) { if (values==null) { return null; } List changes = new ArrayList<>(); for(Map.Entry entry : values.entrySet()) { changes.add(new SingleAttributeChange(entry.getKey(), entry.getValue())); } return changes; } public static class SingleAttributeChange extends AttributeChanges { final String[] values = new String[1]; public SingleAttributeChange(String name, String value) { super(name); values[0] = value; } @Override String[] values() { return values; } } public static class MultipleAttributeChanges extends AttributeChanges { final String[] values; public MultipleAttributeChanges(String name, String[] values) { super(name); this.values = values; } @Override String[] values() { return values; } } public static Class getElementTypeByName(ConfigBeanProxy parent, String elementName) throws ClassNotFoundException { final Dom parentDom = Dom.unwrap(parent); DomDocument document = parentDom.document; ConfigModel.Property a = parentDom.model.elements.get(elementName); if (a!=null) { if (a.isLeaf()) { // dochez : I am not too sure, but that should be a String @Element return null; } else { ConfigModel childModel = ((ConfigModel.Node) a).model; return (Class) childModel.classLoaderHolder.loadClass(childModel.targetTypeName); } } // global lookup ConfigModel model = document.getModelByElementName(elementName); if (model!=null) { return (Class) model.classLoaderHolder.loadClass(model.targetTypeName); } return null; } /** * Unwrap HK2 proxy to ConfigBeanProxy. * @param ConfigBeanProxy probably proxied by HK2. * @return actual ConfigBeanProxy. * @throws MultiException If there was an error resolving the proxy. */ @SuppressWarnings("unchecked") public static T revealProxy(T proxy) { if (proxy instanceof ProxyCtl) { ProxyCtl proxyCtl = (ProxyCtl) proxy; proxy = (T) proxyCtl.__make(); } return proxy; } @Override public Object addChildWithAttributes(ConfigBeanProxy param, ConfigBean parent, Class childType, List attributes, TransactionCallBack runnable) throws TransactionFailure { LOG.finest( () -> String.format("addChildWithAttributes(param=%s, parent=%s, childType=%s, attributes=%s, runnable=%s)", param, parent, childType, attributes, runnable)); // create the child ConfigBeanProxy child = param.createChild(childType); Dom dom = Dom.unwrap(child); // add the child to the parent. WriteableView writeableParent = (WriteableView) Proxy.getInvocationHandler(param); Class parentProxyType = parent.getProxyType(); // first we need to find the element associated with this type ConfigModel.Property element = null; for (ConfigModel.Property e : parent.model.elements.values()) { if (e.isLeaf()) { continue; } ConfigModel elementModel = ((ConfigModel.Node) e).model; LOG.fine(() -> "elementModel.targetTypeName = " + elementModel.targetTypeName + ", collection: " + e.isCollection() + ", childType.getName() = " + childType.getName()); if (elementModel.targetTypeName.equals(childType.getName())) { element = e; break; } else if ( e.isCollection() ) { try { final Class tempClass = elementModel.classLoaderHolder.loadClass(elementModel.targetTypeName); if ( tempClass.isAssignableFrom( childType ) ) { element = e; break; } } catch (Exception ex ) { throw new TransactionFailure("EXCEPTION getting class for " + elementModel.targetTypeName, ex); } } } // now depending whether this is a collection or a single leaf, // we need to process this setting differently if (element != null) { if (element.isCollection()) { // this is kind of nasty, I have to find the method that returns the collection // object because trying to do a element.get without having the parametized List // type will not work. for (Method m : parentProxyType.getMethods()) { final Class returnType = m.getReturnType(); if (Collection.class.isAssignableFrom(returnType) && (m.getParameterTypes().length == 0)) { // this could be it... if (!(m.getGenericReturnType() instanceof ParameterizedType)) { throw new IllegalArgumentException("List needs to be parameterized"); } final Class itemType = Types.erasure(Types.getTypeArgument(m.getGenericReturnType(), 0)); if (itemType.isAssignableFrom(childType)) { List list = null; try { list = (List) m.invoke(param, null); } catch (IllegalAccessException e) { throw new TransactionFailure("Exception while adding to the parent", e); } catch (InvocationTargetException e) { throw new TransactionFailure("Exception while adding to the parent", e); } if (list != null) { list.add(child); break; } } } } } else { // much simpler, I can use the setter directly. writeableParent.setter(element, dom.createProxy(), childType); } } else { throw new TransactionFailure("Parent " + parent.getProxyType() + " does not have a child of type " + childType); } WriteableView writeableChild = (WriteableView) Proxy.getInvocationHandler(child); applyProperties(writeableChild, attributes); dom.addDefaultChildren(); dom.register(); if (runnable!=null) { runnable.performOn(writeableChild); } return child; } private static void applyProperties(WriteableView target, List changes) throws TransactionFailure { if (changes != null) { for (AttributeChanges change : changes) { ConfigModel.Property prop = target.getProperty(change.getName()); if (prop == null) { throw new TransactionFailure("Unknown property name " + change.getName() + " on " + target.getProxyType()); } if (prop.isCollection()) { // we need access to the List try { List list = (List) target.getter(prop, ConfigSupport.class.getDeclaredMethod("defaultPropertyValue", null).getGenericReturnType()); for (String value : change.values()) { list.add(value); } } catch (NoSuchMethodException e) { throw new TransactionFailure(e.getMessage(), e); } } else { target.setter(prop, change.values()[0], String.class); } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy