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

javax.management.modelmbean.ModelMBeanInfoSupport Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) The MX4J Contributors.
 * All rights reserved.
 *
 * This software is distributed under the terms of the MX4J License version 1.0.
 * See the terms of the MX4J License in the documentation provided with this software.
 */

package javax.management.modelmbean;

import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import javax.management.Descriptor;
import javax.management.DescriptorAccess;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanConstructorInfo;
import javax.management.MBeanException;
import javax.management.MBeanFeatureInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanOperationInfo;
import javax.management.RuntimeOperationsException;

/**
 * @version $Revision: 1.18 $
 */
// Change not needed, workaround to a TCK bug only to achieve TCK compliance
// public class ModelMBeanInfoSupport extends MBeanInfo implements ModelMBeanInfo
public class ModelMBeanInfoSupport extends MBeanInfo implements ModelMBeanInfo, Serializable
{
   private static final MBeanAttributeInfo[] EMPTY_ATTRS = {};
   private static final MBeanConstructorInfo[] EMPTY_CTRS = {};
   private static final MBeanNotificationInfo[] EMPTY_NOTIFICATIONS = {};
   private static final MBeanOperationInfo[] EMPTY_OPS = {};

   private static final long serialVersionUID = -1935722590756516193L;

   private Descriptor modelMBeanDescriptor;

   // The following data members are duplicated from parent class, since there is no way for
   // the serialization mechanism to set the deserialized objects to the parent object.
   private MBeanAttributeInfo[] modelMBeanAttributes;
   private MBeanConstructorInfo[] modelMBeanConstructors;
   private MBeanNotificationInfo[] modelMBeanNotifications;
   private MBeanOperationInfo[] modelMBeanOperations;

   public ModelMBeanInfoSupport(String className, String description, ModelMBeanAttributeInfo[] attributes, ModelMBeanConstructorInfo[] constructors, ModelMBeanOperationInfo[] operations, ModelMBeanNotificationInfo[] notifications)
   {
      this(className, description, attributes, constructors, operations, notifications, null);
   }

   public ModelMBeanInfoSupport(String className, String description, ModelMBeanAttributeInfo[] attributes, ModelMBeanConstructorInfo[] constructors, ModelMBeanOperationInfo[] operations, ModelMBeanNotificationInfo[] notifications, Descriptor mbeanDescriptor)
   {
      super(className, description, attributes, constructors, operations, notifications);
      modelMBeanAttributes = attributes != null ? attributes : EMPTY_ATTRS;
      modelMBeanConstructors = constructors != null ? constructors : EMPTY_CTRS;
      modelMBeanNotifications = notifications != null ? notifications : EMPTY_NOTIFICATIONS;
      modelMBeanOperations = operations != null ? operations : EMPTY_OPS;
      checkAndSetDescriptor(mbeanDescriptor);
   }

   public ModelMBeanInfoSupport(ModelMBeanInfo model)
   {
      super(model.getClassName(), model.getDescription(), model.getAttributes(), model.getConstructors(), model.getOperations(), model.getNotifications());
      if (model.getAttributes() != null)
      {
         // cannot assume they are already ModelMBeanAttributeInfo
         MBeanAttributeInfo attributes[] = model.getAttributes();
         modelMBeanAttributes = new ModelMBeanAttributeInfo[attributes.length];
         for (int i = 0; i < attributes.length; i++)
         {
            MBeanAttributeInfo attribute = attributes[i];
            if (attribute instanceof ModelMBeanAttributeInfo)
               modelMBeanAttributes[i] = new ModelMBeanAttributeInfo((ModelMBeanAttributeInfo)attribute);
            else
               modelMBeanAttributes[i] = new ModelMBeanAttributeInfo(attribute.getName(), attribute.getType(), attribute.getDescription(), attribute.isReadable(), attribute.isWritable(), attribute.isIs());
         }
      }
      if (model.getConstructors() != null)
      {
         // cannot assume they are already ModelMBeanConstructorInfo
         MBeanConstructorInfo constructors[] = model.getConstructors();
         modelMBeanConstructors = new ModelMBeanConstructorInfo[constructors.length];
         for (int i = 0; i < constructors.length; i++)
         {
            MBeanConstructorInfo constructor = constructors[i];
            if (constructor instanceof ModelMBeanConstructorInfo)
               modelMBeanConstructors[i] = new ModelMBeanConstructorInfo((ModelMBeanConstructorInfo)constructor);
            else
               modelMBeanConstructors[i] = new ModelMBeanConstructorInfo(constructor.getName(), constructor.getDescription(), constructor.getSignature());
         }
      }
      if (model.getOperations() != null)
      {
         // cannot assume they are already ModelMBeanOperationInfo
         MBeanOperationInfo operations[] = model.getOperations();
         modelMBeanOperations = new ModelMBeanOperationInfo[operations.length];
         for (int i = 0; i < operations.length; i++)
         {
            MBeanOperationInfo operation = operations[i];
            if (operation instanceof ModelMBeanOperationInfo)
               modelMBeanOperations[i] = new ModelMBeanOperationInfo((ModelMBeanOperationInfo)operation);
            else
               modelMBeanOperations[i] = new ModelMBeanOperationInfo(operation.getName(), operation.getDescription(), operation.getSignature(), operation.getReturnType(), operation.getImpact());
         }
      }
      if (model.getNotifications() != null)
      {
         // cannot assume they are already ModelMBeanNotificationInfo
         MBeanNotificationInfo notifications[] = model.getNotifications();
         modelMBeanNotifications = new ModelMBeanNotificationInfo[notifications.length];
         for (int i = 0; i < notifications.length; i++)
         {
            MBeanNotificationInfo notification = notifications[i];
            if (notification instanceof ModelMBeanNotificationInfo)
               modelMBeanNotifications[i] = new ModelMBeanNotificationInfo((ModelMBeanNotificationInfo)notification);
            else
               modelMBeanNotifications[i] = new ModelMBeanNotificationInfo(notification.getNotifTypes(), notification.getName(), notification.getDescription());
         }
      }
      Descriptor mBeanDescriptor = null;
      try
      {
         mBeanDescriptor = model.getMBeanDescriptor();
      }
      catch (Exception e)
      {
         // if there is an exception we use null
      }
      checkAndSetDescriptor(mBeanDescriptor);
   }

   public Object clone()
   {
      return new ModelMBeanInfoSupport(this);
   }

   public Descriptor[] getDescriptors(String type) throws MBeanException, RuntimeOperationsException
   {
      // On the type the 'role' is not used, so for constructor and operation there
      // will be type=constructor and type=operation respectively
      // If type == null, means all descriptors
      if (type == null)
      {
         Descriptor[] attrs = getDescriptors("attribute");
         Descriptor[] opers = getDescriptors("operation");
         Descriptor[] ctors = getDescriptors("constructor");
         Descriptor[] notifs = getDescriptors("notification");
         Descriptor[] all = new Descriptor[attrs.length + opers.length + ctors.length + notifs.length + 1];
         int i = 0;
         all[i] = getMBeanDescriptor();
         ++i;
         System.arraycopy(attrs, 0, all, i, attrs.length);
         i += attrs.length;
         System.arraycopy(opers, 0, all, i, opers.length);
         i += opers.length;
         System.arraycopy(ctors, 0, all, i, ctors.length);
         i += ctors.length;
         System.arraycopy(notifs, 0, all, i, notifs.length);

         return all;
      }
      else if (type.equals("mbean"))
      {
         return new Descriptor[]{getMBeanDescriptor()};
      }
      else if (type.equals("attribute"))
      {
         MBeanAttributeInfo[] attrs = modelMBeanAttributes;
         if (attrs == null)
         {
            return new Descriptor[0];
         }
         Descriptor[] attributes = new Descriptor[attrs.length];
         for (int i = 0; i < attrs.length; ++i)
         {
            ModelMBeanAttributeInfo attr = (ModelMBeanAttributeInfo)attrs[i];
            // It's already cloned
            attributes[i] = attr.getDescriptor();
         }
         return attributes;
      }
      else if (type.equals("operation"))
      {
         MBeanOperationInfo[] opers = modelMBeanOperations;
         if (opers == null)
         {
            return new Descriptor[0];
         }
         Descriptor[] operations = new Descriptor[opers.length];
         for (int i = 0; i < opers.length; ++i)
         {
            ModelMBeanOperationInfo oper = (ModelMBeanOperationInfo)opers[i];
            // It's already cloned
            operations[i] = oper.getDescriptor();
         }
         return operations;
      }
      else if (type.equals("constructor"))
      {
         MBeanConstructorInfo[] ctors = modelMBeanConstructors;
         if (ctors == null)
         {
            return new Descriptor[0];
         }
         Descriptor[] constructors = new Descriptor[ctors.length];
         for (int i = 0; i < ctors.length; ++i)
         {
            ModelMBeanConstructorInfo ctor = (ModelMBeanConstructorInfo)ctors[i];
            // It's already cloned
            constructors[i] = ctor.getDescriptor();
         }
         return constructors;
      }
      else if (type.equals("notification"))
      {
         MBeanNotificationInfo[] notifs = modelMBeanNotifications;
         if (notifs == null)
         {
            return new Descriptor[0];
         }
         Descriptor[] notifications = new Descriptor[notifs.length];
         for (int i = 0; i < notifs.length; ++i)
         {
            ModelMBeanNotificationInfo notif = (ModelMBeanNotificationInfo)notifs[i];
            // It's already cloned
            notifications[i] = notif.getDescriptor();
         }
         return notifications;
      }
      else
      {
         throw new RuntimeOperationsException(new IllegalArgumentException("Invalid descriptor type"));
      }
   }

   public void setDescriptors(Descriptor[] descriptors) throws MBeanException, RuntimeOperationsException
   {
      if (descriptors == null)
      {
         throw new RuntimeOperationsException(new IllegalArgumentException("Descriptors cannot be null"));
      }
      RuntimeOperationsException x = null;
      for (int i = 0; i < descriptors.length; ++i)
      {
         // PENDING: what should I do in case of exception setting one descriptor ?
         // Going on with the other descriptors or let the exception out ?
         try
         {
            setDescriptor(descriptors[i], null);
         }
         catch (RuntimeOperationsException ignored)
         {
            x = ignored;
         }
      }
      // PENDING: don't know if this is a suitable solution, anyhow...
      if (x != null)
      {
         throw x;
      }
   }

   public Descriptor getDescriptor(String name) throws MBeanException, RuntimeOperationsException
   {
      return getDescriptor(name, null);
   }

   public Descriptor getDescriptor(String name, String type) throws MBeanException, RuntimeOperationsException
   {
      if (name == null)
      {
         throw new RuntimeOperationsException(new IllegalArgumentException("Descriptor name cannot be null"));
      }
      //if (type == null) {throw new RuntimeOperationsException(new IllegalArgumentException("Descriptor type cannot be null"));}

      if ("MBean".equals(type))
      {
         return getMBeanDescriptor();
      }
      else if (type != null)
      {
         Descriptor[] descrs = getDescriptors(type);
         for (int i = 0; i < descrs.length; ++i)
         {
            Descriptor descr = descrs[i];
            if (name.equals(descr.getFieldValue("name")))
            {
               // Found, no need to clone it.
               return descr;
            }
         }
      }
      else
      {
         // will have to check them all
         Descriptor result = findDescriptorByName(modelMBeanAttributes, name);
         if (result != null)
         {
            return result;
         }
         result = findDescriptorByName(modelMBeanConstructors, name);
         if (result != null)
         {
            return result;
         }
         result = findDescriptorByName(modelMBeanNotifications, name);
         if (result != null)
         {
            return result;
         }
         result = findDescriptorByName(modelMBeanOperations, name);
         if (result != null)
         {
            return result;
         }
      }
      return null;
   }

   public void setDescriptor(Descriptor descriptor, String descriptorType) throws MBeanException, RuntimeOperationsException
   {
      // PENDING: should throw instead of returning ?
      if (descriptor == null)
      {
         return;
      }
      if (descriptorType == null)
      {
         descriptorType = (String)descriptor.getFieldValue("descriptorType");
         // Still null ?
         if (descriptorType == null)
         {
            throw new RuntimeOperationsException(new IllegalArgumentException("Field descriptorType in the given descriptor is not valid"));
         }

         if (descriptorType.equals("operation"))
         {
            // Take the role to distinguish between operation and constructor
            String role = (String)descriptor.getFieldValue("role");
            if (role == null)
            {
               throw new RuntimeOperationsException(new IllegalArgumentException("Field role in the given descriptor is not valid"));
            }

            descriptorType = role;
         }
      }

      String name = (String)descriptor.getFieldValue("name");
      if (name == null)
      {
         throw new RuntimeOperationsException(new IllegalArgumentException("Field name in the given descriptor is not valid"));
      }

      // Now decide from the descriptor type where to find the right descriptor
      if (descriptorType.equals("MBean"))
      {
         setMBeanDescriptor(descriptor);
      }
      else if (descriptorType.equals("attribute"))
      {
         MBeanAttributeInfo[] attrs = modelMBeanAttributes;
         if (attrs != null)
         {
            for (int i = 0; i < attrs.length; ++i)
            {
               ModelMBeanAttributeInfo attr = (ModelMBeanAttributeInfo)attrs[i];
               if (name.equals(attr.getName()))
               {
                  // Found the right one
                  attr.setDescriptor(descriptor);
                  break;
               }
            }
         }
      }
      else if (descriptorType.equals("notification"))
      {
         MBeanNotificationInfo[] notifs = modelMBeanNotifications;
         if (notifs != null)
         {
            for (int i = 0; i < notifs.length; ++i)
            {
               ModelMBeanNotificationInfo notif = (ModelMBeanNotificationInfo)notifs[i];
               if (name.equals(notif.getName()))
               {
                  // Found the right one
                  notif.setDescriptor(descriptor);
                  break;
               }
            }
         }
      }
      else if (descriptorType.equals("constructor"))
      {
         MBeanConstructorInfo[] ctors = modelMBeanConstructors;
         if (ctors != null)
         {
            for (int i = 0; i < ctors.length; ++i)
            {
               ModelMBeanConstructorInfo ctor = (ModelMBeanConstructorInfo)ctors[i];
               if (name.equals(ctor.getName()))
               {
                  // Found the right one
                  ctor.setDescriptor(descriptor);
                  break;
               }
            }
         }
      }
      else if (descriptorType.equals("operation")/* || descriptorType.equals("getter") || descriptorType.equals("setter")*/)
      {
         MBeanOperationInfo[] opers = modelMBeanOperations;
         if (opers != null)
         {
            for (int i = 0; i < opers.length; ++i)
            {
               ModelMBeanOperationInfo oper = (ModelMBeanOperationInfo)opers[i];
               if (name.equals(oper.getName()))
               {
                  // Found the right one
                  oper.setDescriptor(descriptor);
                  break;
               }
            }
         }
      }
   }

   public ModelMBeanAttributeInfo getAttribute(String name) throws MBeanException, RuntimeOperationsException
   {
      if (name == null)
      {
         throw new RuntimeOperationsException(new IllegalArgumentException("Name argument cannot be null"));
      }
      MBeanAttributeInfo[] attrs = modelMBeanAttributes;
      if (attrs != null)
      {
         for (int i = 0; i < attrs.length; ++i)
         {
            ModelMBeanAttributeInfo attr = (ModelMBeanAttributeInfo)attrs[i];
            if (name.equals(attr.getName()))
            {
               // Clone, since the returned attribute is modifiable
               return (ModelMBeanAttributeInfo)attr.clone();
            }
         }
      }
      // Not found, return null
      return null;
   }

   public ModelMBeanOperationInfo getOperation(String name) throws MBeanException, RuntimeOperationsException
   {
      if (name == null)
      {
         throw new RuntimeOperationsException(new IllegalArgumentException("Name argument cannot be null"));
      }
      MBeanOperationInfo[] opers = modelMBeanOperations;
      if (opers != null)
      {
         for (int i = 0; i < opers.length; ++i)
         {
            ModelMBeanOperationInfo oper = (ModelMBeanOperationInfo)opers[i];
            if (name.equals(oper.getName()))
            {
               // Clone, since the returned operation is modifiable
               return (ModelMBeanOperationInfo)oper.clone();
            }
         }
      }
      // Not found, return null
      return null;
   }

   public ModelMBeanConstructorInfo getConstructor(String name) throws MBeanException, RuntimeOperationsException
   {
      if (name == null)
      {
         throw new RuntimeOperationsException(new IllegalArgumentException("Name argument cannot be null"));
      }
      MBeanConstructorInfo[] ctors = modelMBeanConstructors;
      if (ctors != null)
      {
         for (int i = 0; i < ctors.length; ++i)
         {
            ModelMBeanConstructorInfo ctor = (ModelMBeanConstructorInfo)ctors[i];
            if (name.equals(ctor.getName()))
            {
               // Clone, since the returned operation is modifiable
               return (ModelMBeanConstructorInfo)ctor.clone();
            }
         }
      }
      // Not found, return null
      return null;
   }

   public ModelMBeanNotificationInfo getNotification(String name) throws MBeanException, RuntimeOperationsException
   {
      if (name == null)
      {
         throw new RuntimeOperationsException(new IllegalArgumentException("Name argument cannot be null"));
      }
      MBeanNotificationInfo[] notifs = modelMBeanNotifications;
      if (notifs != null)
      {
         for (int i = 0; i < notifs.length; ++i)
         {
            ModelMBeanNotificationInfo notif = (ModelMBeanNotificationInfo)notifs[i];
            if (name.equals(notif.getName()))
            {
               // Clone, since the returned operation is modifiable
               return (ModelMBeanNotificationInfo)notif.clone();
            }
         }
      }
      // Not found, return null
      return null;
   }

   public Descriptor getMBeanDescriptor() throws MBeanException, RuntimeOperationsException
   {
      return (Descriptor)modelMBeanDescriptor.clone();
   }

   public void setMBeanDescriptor(Descriptor descriptor) throws MBeanException, RuntimeOperationsException
   {
      if (descriptor == null)
      {
         // Replace with default descriptor
         modelMBeanDescriptor = createDefaultMBeanDescriptor();
      }
      else
      {
         if (isDescriptorValid(descriptor))
         {
            modelMBeanDescriptor = (Descriptor)descriptor.clone();
         }
         else
         {
            throw new RuntimeOperationsException(new IllegalArgumentException("Invalid descriptor"));
         }
      }
   }

   public MBeanConstructorInfo[] getConstructors()
   {
      // I should clone, since MBeanConstructorInfo is immutable, but ModelMBeanConstructorInfo it isn't
      MBeanConstructorInfo[] ctors = modelMBeanConstructors;
      if (ctors == null)
      {
         return null;
      }
      ModelMBeanConstructorInfo[] constructors = new ModelMBeanConstructorInfo[ctors.length];
      for (int i = 0; i < ctors.length; ++i)
      {
         ModelMBeanConstructorInfo ctor = (ModelMBeanConstructorInfo)ctors[i];
         constructors[i] = (ModelMBeanConstructorInfo)ctor.clone();
      }
      return constructors;
   }

   public MBeanAttributeInfo[] getAttributes()
   {
      // I should clone, since MBeanAttributeInfo is immutable, but ModelMBeanAttributeInfo it isn't
      MBeanAttributeInfo[] attrs = modelMBeanAttributes;
      if (attrs == null)
      {
         return null;
      }
      ModelMBeanAttributeInfo[] attributes = new ModelMBeanAttributeInfo[attrs.length];
      for (int i = 0; i < attrs.length; ++i)
      {
         ModelMBeanAttributeInfo attr = (ModelMBeanAttributeInfo)attrs[i];
         attributes[i] = (ModelMBeanAttributeInfo)attr.clone();
      }
      return attributes;
   }

   public MBeanOperationInfo[] getOperations()
   {
      // I should clone, since MBeanOperationInfo is immutable, but ModelMBeanOperationInfo it isn't
      MBeanOperationInfo[] opers = modelMBeanOperations;
      if (opers == null)
      {
         return null;
      }
      ModelMBeanOperationInfo[] operations = new ModelMBeanOperationInfo[opers.length];
      for (int i = 0; i < opers.length; ++i)
      {
         ModelMBeanOperationInfo oper = (ModelMBeanOperationInfo)opers[i];
         operations[i] = (ModelMBeanOperationInfo)oper.clone();
      }
      return operations;
   }

   public MBeanNotificationInfo[] getNotifications()
   {
      // I should clone, since MBeanNotificationInfo is immutable, but ModelMBeanNotificationInfo it isn't
      MBeanNotificationInfo[] notifs = modelMBeanNotifications;
      if (notifs == null)
      {
         return null;
      }
      ModelMBeanNotificationInfo[] notifications = new ModelMBeanNotificationInfo[notifs.length];
      for (int i = 0; i < notifs.length; ++i)
      {
         ModelMBeanNotificationInfo notif = (ModelMBeanNotificationInfo)notifs[i];
         notifications[i] = (ModelMBeanNotificationInfo)notif.clone();
      }
      return notifications;
   }

   private void checkAndSetDescriptor(Descriptor descriptor)
   {
      if (descriptor == null)
      {
         modelMBeanDescriptor = createDefaultMBeanDescriptor();
      }
      else if (isDescriptorValid(descriptor))
      {
         modelMBeanDescriptor = addRequiredFields(descriptor);
      }
      else
      {
         throw new RuntimeOperationsException(new IllegalArgumentException("Invalid Descriptor"));
      }
   }

   private boolean isDescriptorValid(Descriptor descriptor)
   {
      if (!descriptor.isValid())
      {
         return false;
      }

      // Spec compliance checks

      // Mandatory fields are: name, descriptorType, persistLocation(?), persistName(?), log(?), logFile(?)
      String[] names = descriptor.getFieldNames();

      if (!containsIgnoreCase(names, "name") ||
          !containsIgnoreCase(names, "descriptortype")/* ||
         !containsIgnoreCase(names, "persistlocation") ||
         !containsIgnoreCase(names, "persistname") ||
         !containsIgnoreCase(names, "log") ||
         !containsIgnoreCase(names, "logfile")*/)
      {
         return false;
      }

      // The spec is unclear on what the field name should contain: the name of the MBean or its class name ?
      // For now I stay loose, but since it is a dynamic MBean the className cannot be null; this check will be done
      // by RequiredModelMBean
//		String name = getClassName();
//		if (name == null) {return false;}
//		if (!name.equals(descriptor.getFieldValue("name"))) {return false;}

      // Descriptor type must be 'MBean'
      String desctype = (String)descriptor.getFieldValue("descriptortype");
      if (desctype.compareToIgnoreCase("mbean") != 0) return false;

      return true;
   }

   private Descriptor createDefaultMBeanDescriptor()
   {
      // The spec and the javadoc are misaligned WRT the default mbean descriptor:
      // Spec says the values of fields are case sensitive, javadoc does not care...
      // For field 'export', spec says that a value of null means not visible to other Agent and that any other value means
      // that is visible, while javadoc says 'F' means not visible...
      // Go with the Javadoc to mimic the RI (WkH)
      String[] names = new String[]{"name", "descriptorType", "displayName", "persistPolicy", "log", "export", "visibility"};
      int index = getClassName().lastIndexOf('.') + 1;
      Object[] values = new Object[]{getClassName().substring(index), "MBean", getClassName(), "Never", "F", "F", "1"};
      return new DescriptorSupport(names, values);
   }


   private Descriptor findDescriptorByName(MBeanFeatureInfo[] features, String name)
   {
      if (features != null)
      {
         for (int i = 0; i < features.length; ++i)
         {
            MBeanFeatureInfo feature = features[i];
            if (feature != null && feature.getName().equals(name) && feature instanceof DescriptorAccess)
            {
               return ((DescriptorAccess)feature).getDescriptor();
            }
         }
      }
      return null;
   }

   private Descriptor addRequiredFields(Descriptor d)
   {
      Descriptor result = (Descriptor)d.clone();
      String[] reqfields = {
         "displayname",
         "persistpolicy",
         "log",
         "export",
         "visibility"
      };
      String[] defvalues = {
         (String)d.getFieldValue("name"),
         "never",
         "F",
         "F",
         "1"
      };
      List fields = Arrays.asList(d.getFieldNames());
      for (int i = 0; i < reqfields.length; i++)
      {
         if (fields.contains(reqfields[i]) == false)
         {
            result.setField(reqfields[i], defvalues[i]);
         }
      }
      return result;
   }

   static boolean containsIgnoreCase(String[] fields, String field)
   {
      for (int i = 0; i < fields.length; ++i)
      {
         if (fields[i].equalsIgnoreCase(field)) return true;
      }
      return false;
   }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy