Created
April 12, 2012 12:14
-
-
Save nickman/2366862 to your computer and use it in GitHub Desktop.
An example of an MBeanServer interceptor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.helios.jmx; | |
import java.io.ObjectInputStream; | |
import java.lang.management.ManagementFactory; | |
import java.util.Set; | |
import javax.management.Attribute; | |
import javax.management.AttributeList; | |
import javax.management.AttributeNotFoundException; | |
import javax.management.InstanceAlreadyExistsException; | |
import javax.management.InstanceNotFoundException; | |
import javax.management.IntrospectionException; | |
import javax.management.InvalidAttributeValueException; | |
import javax.management.ListenerNotFoundException; | |
import javax.management.MBeanException; | |
import javax.management.MBeanInfo; | |
import javax.management.MBeanRegistrationException; | |
import javax.management.MBeanServer; | |
import javax.management.MBeanServerBuilder; | |
import javax.management.MBeanServerDelegate; | |
import javax.management.MBeanServerFactory; | |
import javax.management.NotCompliantMBeanException; | |
import javax.management.NotificationFilter; | |
import javax.management.NotificationListener; | |
import javax.management.ObjectInstance; | |
import javax.management.ObjectName; | |
import javax.management.OperationsException; | |
import javax.management.QueryExp; | |
import javax.management.ReflectionException; | |
import javax.management.loading.ClassLoaderRepository; | |
/** | |
* <p>Title: HeliosMBeanServerBuilder</p> | |
* <p>Description: Builds a intercepting MBeanServer</p> | |
* <p>Company: Helios Development Group LLC</p> | |
* @author Whitehead (nwhitehead AT heliosdev DOT org) | |
* <p><code>org.helios.jmx.HeliosMBeanServerBuilder</code></p> | |
*/ | |
public class HeliosMBeanServerBuilder extends MBeanServerBuilder { | |
/** The ObjectName filter that defines which ObjectNames are renamed */ | |
protected final ObjectName filter; | |
/** The ObjectName rename strategy */ | |
protected final IRenameStrategy renameStartegy; | |
/** The system property name for the boot time defined filter */ | |
public static final String PROP_FILTER = "helios.jmx.renamer.filter"; | |
/** The system property name for the default filter */ | |
public static final String DEFAULT_FILTER = "*:*"; | |
/** The system property name for the boot time defined renaming strategy */ | |
public static final String PROP_STRATEGY = "helios.jmx.renamer.strategy"; | |
/** The system property name for the default filter */ | |
public static final String DEFAULT_STRATEGY = AppendSerialRenamer.class.getName(); | |
// -Dhelios.jmx.renamer.filter=java.util.logging:* | |
/** | |
* @param args | |
*/ | |
public static void main(String[] args) { | |
System.out.println("MBeanServer Interceptor Test"); | |
// Iterate through all the registered platform MBeanServer object names | |
for(ObjectName o: ManagementFactory.getPlatformMBeanServer().queryNames(null, null)) { | |
System.out.println(o); | |
} | |
// Wait so we can browse the MBeans in JConsole | |
try { Thread.currentThread().join(); } catch (Exception e) {} | |
} | |
/** | |
* Creates a new HeliosMBeanServerBuilder | |
* @param filter The ObjectName renaming filter | |
* @param renameStartegy The ObjectName renaming strategy | |
*/ | |
public HeliosMBeanServerBuilder(ObjectName filter, IRenameStrategy renameStartegy) { | |
this.filter = filter; | |
this.renameStartegy = renameStartegy; | |
} | |
/** | |
* Creates a new HeliosMBeanServerBuilder using a system property defined filter and strategy | |
*/ | |
public HeliosMBeanServerBuilder() { | |
this(objectName(System.getProperty(PROP_FILTER, DEFAULT_FILTER)), resolveStrategy(System.getProperty(PROP_STRATEGY, DEFAULT_STRATEGY))); | |
} | |
/** | |
* Safe ObjectName creator | |
* @param name The ObjectName string | |
* @return an ObjectName | |
*/ | |
public static ObjectName objectName(String name) { | |
if(name==null) throw new IllegalArgumentException("The passed name was null", new Throwable()); | |
try { | |
return new ObjectName(name); | |
} catch (Exception e) { | |
throw new RuntimeException("Failed to create ObjectName from [" + name + "]", e); | |
} | |
} | |
/** | |
* Resolves the passed class name into an IRenameStrategy instance | |
* @param className The IRenameStrategy class name | |
* @return an IRenameStrategy instance | |
*/ | |
public static IRenameStrategy resolveStrategy(String className) { | |
try { | |
return (IRenameStrategy)Class.forName(className).newInstance(); | |
} catch (Exception e) { | |
throw new RuntimeException("Failed to create renamer instance [" + className + "]", e); | |
} | |
} | |
/** | |
* {@inheritDoc} | |
* @see javax.management.MBeanServerBuilder#newMBeanServer(java.lang.String, javax.management.MBeanServer, javax.management.MBeanServerDelegate) | |
*/ | |
public MBeanServer newMBeanServer(String defaultDomain, MBeanServer outer, MBeanServerDelegate delegate) { | |
return new RenamingMBeanServer(super.newMBeanServer(defaultDomain, outer, delegate), renameStartegy, filter); | |
} | |
/** | |
* <p>Title: IRenameStrategy</p> | |
* <p>Description: Defines a class that can rename an ObjectName using a specific pattern</p> | |
* <p>Company: Helios Development Group LLC</p> | |
* @author Whitehead (nwhitehead AT heliosdev DOT org) | |
* <p><code>org.helios.jmx.HeliosMBeanServerBuilder.IRenameStrategy</code></p> | |
*/ | |
public interface IRenameStrategy { | |
/** | |
* Modifies the passed ObjectName to a new name | |
* @param toBeRenamed The ObjectName to rename | |
* @return the renamed ObjectName | |
*/ | |
public ObjectName rename(ObjectName toBeRenamed); | |
} | |
/** | |
* <p>Title: AppendSerialRenamer</p> | |
* <p>Description: An IRenameStrategy that appends the hash code of the original ObjectName sting</p> | |
* <p>Company: Helios Development Group LLC</p> | |
* @author Whitehead (nwhitehead AT heliosdev DOT org) | |
* <p><code>org.helios.jmx.HeliosMBeanServerBuilder.AppendSerialRenamer</code></p> | |
*/ | |
public static class AppendSerialRenamer implements IRenameStrategy { | |
public ObjectName rename(ObjectName toBeRenamed) { | |
if(toBeRenamed==null) throw new IllegalArgumentException("The passed object name was null", new Throwable()); | |
try { | |
int serial = toBeRenamed.toString().hashCode(); | |
return new ObjectName(toBeRenamed.toString() + ",serial=" + serial); | |
} catch (Exception e) { | |
throw new RuntimeException("Failed to rename ObjectName [" + toBeRenamed + "]", e); | |
} | |
} | |
} | |
/** | |
* <p>Title: RenamingMBeanServer</p> | |
* <p>Description: An MBeanServer wrapper that renames matching ObjectNames of registered MBeans</p> | |
* <p>Company: Helios Development Group LLC</p> | |
* @author Whitehead (nwhitehead AT heliosdev DOT org) | |
* <p><code>org.helios.jmx.HeliosMBeanServerBuilder.RenamingMBeanServer</code></p> | |
*/ | |
public static class RenamingMBeanServer implements MBeanServer { | |
/** The actual MBeanServer delegate */ | |
protected final MBeanServer inner; | |
/** The ObjectName renaming strategy */ | |
protected final IRenameStrategy renamer; | |
/** The filter that determines which ObjectNames should be filtered */ | |
protected final ObjectName filter; | |
/** | |
* Creates a new RenamingMBeanServer | |
* @param inner | |
* @param renamer | |
* @param filter | |
*/ | |
public RenamingMBeanServer(MBeanServer inner, IRenameStrategy renamer, ObjectName filter) { | |
this.inner = inner; | |
this.renamer = renamer; | |
this.filter = filter; | |
} | |
/** | |
* @param object | |
* @param name | |
* @return | |
* @throws InstanceAlreadyExistsException | |
* @throws MBeanRegistrationException | |
* @throws NotCompliantMBeanException | |
* @see javax.management.MBeanServer#registerMBean(java.lang.Object, javax.management.ObjectName) | |
*/ | |
public ObjectInstance registerMBean(Object object, ObjectName name) | |
throws InstanceAlreadyExistsException, | |
MBeanRegistrationException, NotCompliantMBeanException { | |
if(filter.apply(name)) { | |
name = renamer.rename(name); | |
} | |
return inner.registerMBean(object, name); | |
} | |
// ======================================================================= | |
/** | |
* @param className | |
* @param name | |
* @return | |
* @throws ReflectionException | |
* @throws InstanceAlreadyExistsException | |
* @throws MBeanRegistrationException | |
* @throws MBeanException | |
* @throws NotCompliantMBeanException | |
* @see javax.management.MBeanServer#createMBean(java.lang.String, javax.management.ObjectName) | |
*/ | |
public ObjectInstance createMBean(String className, ObjectName name) | |
throws ReflectionException, InstanceAlreadyExistsException, | |
MBeanRegistrationException, MBeanException, | |
NotCompliantMBeanException { | |
return inner.createMBean(className, name); | |
} | |
/** | |
* @param className | |
* @param name | |
* @param loaderName | |
* @return | |
* @throws ReflectionException | |
* @throws InstanceAlreadyExistsException | |
* @throws MBeanRegistrationException | |
* @throws MBeanException | |
* @throws NotCompliantMBeanException | |
* @throws InstanceNotFoundException | |
* @see javax.management.MBeanServer#createMBean(java.lang.String, javax.management.ObjectName, javax.management.ObjectName) | |
*/ | |
public ObjectInstance createMBean(String className, ObjectName name, | |
ObjectName loaderName) throws ReflectionException, | |
InstanceAlreadyExistsException, MBeanRegistrationException, | |
MBeanException, NotCompliantMBeanException, | |
InstanceNotFoundException { | |
return inner.createMBean(className, name, loaderName); | |
} | |
/** | |
* @param className | |
* @param name | |
* @param params | |
* @param signature | |
* @return | |
* @throws ReflectionException | |
* @throws InstanceAlreadyExistsException | |
* @throws MBeanRegistrationException | |
* @throws MBeanException | |
* @throws NotCompliantMBeanException | |
* @see javax.management.MBeanServer#createMBean(java.lang.String, javax.management.ObjectName, java.lang.Object[], java.lang.String[]) | |
*/ | |
public ObjectInstance createMBean(String className, ObjectName name, | |
Object[] params, String[] signature) | |
throws ReflectionException, InstanceAlreadyExistsException, | |
MBeanRegistrationException, MBeanException, | |
NotCompliantMBeanException { | |
return inner.createMBean(className, name, params, signature); | |
} | |
/** | |
* @param className | |
* @param name | |
* @param loaderName | |
* @param params | |
* @param signature | |
* @return | |
* @throws ReflectionException | |
* @throws InstanceAlreadyExistsException | |
* @throws MBeanRegistrationException | |
* @throws MBeanException | |
* @throws NotCompliantMBeanException | |
* @throws InstanceNotFoundException | |
* @see javax.management.MBeanServer#createMBean(java.lang.String, javax.management.ObjectName, javax.management.ObjectName, java.lang.Object[], java.lang.String[]) | |
*/ | |
public ObjectInstance createMBean(String className, ObjectName name, | |
ObjectName loaderName, Object[] params, String[] signature) | |
throws ReflectionException, InstanceAlreadyExistsException, | |
MBeanRegistrationException, MBeanException, | |
NotCompliantMBeanException, InstanceNotFoundException { | |
return inner.createMBean(className, name, loaderName, params, | |
signature); | |
} | |
/** | |
* @param name | |
* @throws InstanceNotFoundException | |
* @throws MBeanRegistrationException | |
* @see javax.management.MBeanServer#unregisterMBean(javax.management.ObjectName) | |
*/ | |
public void unregisterMBean(ObjectName name) | |
throws InstanceNotFoundException, MBeanRegistrationException { | |
inner.unregisterMBean(name); | |
} | |
/** | |
* @param name | |
* @return | |
* @throws InstanceNotFoundException | |
* @see javax.management.MBeanServer#getObjectInstance(javax.management.ObjectName) | |
*/ | |
public ObjectInstance getObjectInstance(ObjectName name) | |
throws InstanceNotFoundException { | |
return inner.getObjectInstance(name); | |
} | |
/** | |
* @param name | |
* @param query | |
* @return | |
* @see javax.management.MBeanServer#queryMBeans(javax.management.ObjectName, javax.management.QueryExp) | |
*/ | |
public Set<ObjectInstance> queryMBeans(ObjectName name, QueryExp query) { | |
return inner.queryMBeans(name, query); | |
} | |
/** | |
* @param name | |
* @param query | |
* @return | |
* @see javax.management.MBeanServer#queryNames(javax.management.ObjectName, javax.management.QueryExp) | |
*/ | |
public Set<ObjectName> queryNames(ObjectName name, QueryExp query) { | |
return inner.queryNames(name, query); | |
} | |
/** | |
* @param name | |
* @return | |
* @see javax.management.MBeanServer#isRegistered(javax.management.ObjectName) | |
*/ | |
public boolean isRegistered(ObjectName name) { | |
return inner.isRegistered(name); | |
} | |
/** | |
* @return | |
* @see javax.management.MBeanServer#getMBeanCount() | |
*/ | |
public Integer getMBeanCount() { | |
return inner.getMBeanCount(); | |
} | |
/** | |
* @param name | |
* @param attribute | |
* @return | |
* @throws MBeanException | |
* @throws AttributeNotFoundException | |
* @throws InstanceNotFoundException | |
* @throws ReflectionException | |
* @see javax.management.MBeanServer#getAttribute(javax.management.ObjectName, java.lang.String) | |
*/ | |
public Object getAttribute(ObjectName name, String attribute) | |
throws MBeanException, AttributeNotFoundException, | |
InstanceNotFoundException, ReflectionException { | |
return inner.getAttribute(name, attribute); | |
} | |
/** | |
* @param name | |
* @param attributes | |
* @return | |
* @throws InstanceNotFoundException | |
* @throws ReflectionException | |
* @see javax.management.MBeanServer#getAttributes(javax.management.ObjectName, java.lang.String[]) | |
*/ | |
public AttributeList getAttributes(ObjectName name, String[] attributes) | |
throws InstanceNotFoundException, ReflectionException { | |
return inner.getAttributes(name, attributes); | |
} | |
/** | |
* @param name | |
* @param attribute | |
* @throws InstanceNotFoundException | |
* @throws AttributeNotFoundException | |
* @throws InvalidAttributeValueException | |
* @throws MBeanException | |
* @throws ReflectionException | |
* @see javax.management.MBeanServer#setAttribute(javax.management.ObjectName, javax.management.Attribute) | |
*/ | |
public void setAttribute(ObjectName name, Attribute attribute) | |
throws InstanceNotFoundException, AttributeNotFoundException, | |
InvalidAttributeValueException, MBeanException, | |
ReflectionException { | |
inner.setAttribute(name, attribute); | |
} | |
/** | |
* @param name | |
* @param attributes | |
* @return | |
* @throws InstanceNotFoundException | |
* @throws ReflectionException | |
* @see javax.management.MBeanServer#setAttributes(javax.management.ObjectName, javax.management.AttributeList) | |
*/ | |
public AttributeList setAttributes(ObjectName name, | |
AttributeList attributes) throws InstanceNotFoundException, | |
ReflectionException { | |
return inner.setAttributes(name, attributes); | |
} | |
/** | |
* @param name | |
* @param operationName | |
* @param params | |
* @param signature | |
* @return | |
* @throws InstanceNotFoundException | |
* @throws MBeanException | |
* @throws ReflectionException | |
* @see javax.management.MBeanServer#invoke(javax.management.ObjectName, java.lang.String, java.lang.Object[], java.lang.String[]) | |
*/ | |
public Object invoke(ObjectName name, String operationName, | |
Object[] params, String[] signature) | |
throws InstanceNotFoundException, MBeanException, | |
ReflectionException { | |
return inner.invoke(name, operationName, params, signature); | |
} | |
/** | |
* @return | |
* @see javax.management.MBeanServer#getDefaultDomain() | |
*/ | |
public String getDefaultDomain() { | |
return inner.getDefaultDomain(); | |
} | |
/** | |
* @return | |
* @see javax.management.MBeanServer#getDomains() | |
*/ | |
public String[] getDomains() { | |
return inner.getDomains(); | |
} | |
/** | |
* @param name | |
* @param listener | |
* @param filter | |
* @param handback | |
* @throws InstanceNotFoundException | |
* @see javax.management.MBeanServer#addNotificationListener(javax.management.ObjectName, javax.management.NotificationListener, javax.management.NotificationFilter, java.lang.Object) | |
*/ | |
public void addNotificationListener(ObjectName name, | |
NotificationListener listener, NotificationFilter filter, | |
Object handback) throws InstanceNotFoundException { | |
inner.addNotificationListener(name, listener, filter, handback); | |
} | |
/** | |
* @param name | |
* @param listener | |
* @param filter | |
* @param handback | |
* @throws InstanceNotFoundException | |
* @see javax.management.MBeanServer#addNotificationListener(javax.management.ObjectName, javax.management.ObjectName, javax.management.NotificationFilter, java.lang.Object) | |
*/ | |
public void addNotificationListener(ObjectName name, | |
ObjectName listener, NotificationFilter filter, Object handback) | |
throws InstanceNotFoundException { | |
inner.addNotificationListener(name, listener, filter, handback); | |
} | |
/** | |
* @param name | |
* @param listener | |
* @throws InstanceNotFoundException | |
* @throws ListenerNotFoundException | |
* @see javax.management.MBeanServer#removeNotificationListener(javax.management.ObjectName, javax.management.ObjectName) | |
*/ | |
public void removeNotificationListener(ObjectName name, | |
ObjectName listener) throws InstanceNotFoundException, | |
ListenerNotFoundException { | |
inner.removeNotificationListener(name, listener); | |
} | |
/** | |
* @param name | |
* @param listener | |
* @param filter | |
* @param handback | |
* @throws InstanceNotFoundException | |
* @throws ListenerNotFoundException | |
* @see javax.management.MBeanServer#removeNotificationListener(javax.management.ObjectName, javax.management.ObjectName, javax.management.NotificationFilter, java.lang.Object) | |
*/ | |
public void removeNotificationListener(ObjectName name, | |
ObjectName listener, NotificationFilter filter, Object handback) | |
throws InstanceNotFoundException, ListenerNotFoundException { | |
inner.removeNotificationListener(name, listener, filter, handback); | |
} | |
/** | |
* @param name | |
* @param listener | |
* @throws InstanceNotFoundException | |
* @throws ListenerNotFoundException | |
* @see javax.management.MBeanServer#removeNotificationListener(javax.management.ObjectName, javax.management.NotificationListener) | |
*/ | |
public void removeNotificationListener(ObjectName name, | |
NotificationListener listener) | |
throws InstanceNotFoundException, ListenerNotFoundException { | |
inner.removeNotificationListener(name, listener); | |
} | |
/** | |
* @param name | |
* @param listener | |
* @param filter | |
* @param handback | |
* @throws InstanceNotFoundException | |
* @throws ListenerNotFoundException | |
* @see javax.management.MBeanServer#removeNotificationListener(javax.management.ObjectName, javax.management.NotificationListener, javax.management.NotificationFilter, java.lang.Object) | |
*/ | |
public void removeNotificationListener(ObjectName name, | |
NotificationListener listener, NotificationFilter filter, | |
Object handback) throws InstanceNotFoundException, | |
ListenerNotFoundException { | |
inner.removeNotificationListener(name, listener, filter, handback); | |
} | |
/** | |
* @param name | |
* @return | |
* @throws InstanceNotFoundException | |
* @throws IntrospectionException | |
* @throws ReflectionException | |
* @see javax.management.MBeanServer#getMBeanInfo(javax.management.ObjectName) | |
*/ | |
public MBeanInfo getMBeanInfo(ObjectName name) | |
throws InstanceNotFoundException, IntrospectionException, | |
ReflectionException { | |
return inner.getMBeanInfo(name); | |
} | |
/** | |
* @param name | |
* @param className | |
* @return | |
* @throws InstanceNotFoundException | |
* @see javax.management.MBeanServer#isInstanceOf(javax.management.ObjectName, java.lang.String) | |
*/ | |
public boolean isInstanceOf(ObjectName name, String className) | |
throws InstanceNotFoundException { | |
return inner.isInstanceOf(name, className); | |
} | |
/** | |
* @param className | |
* @return | |
* @throws ReflectionException | |
* @throws MBeanException | |
* @see javax.management.MBeanServer#instantiate(java.lang.String) | |
*/ | |
public Object instantiate(String className) throws ReflectionException, | |
MBeanException { | |
return inner.instantiate(className); | |
} | |
/** | |
* @param className | |
* @param loaderName | |
* @return | |
* @throws ReflectionException | |
* @throws MBeanException | |
* @throws InstanceNotFoundException | |
* @see javax.management.MBeanServer#instantiate(java.lang.String, javax.management.ObjectName) | |
*/ | |
public Object instantiate(String className, ObjectName loaderName) | |
throws ReflectionException, MBeanException, | |
InstanceNotFoundException { | |
return inner.instantiate(className, loaderName); | |
} | |
/** | |
* @param className | |
* @param params | |
* @param signature | |
* @return | |
* @throws ReflectionException | |
* @throws MBeanException | |
* @see javax.management.MBeanServer#instantiate(java.lang.String, java.lang.Object[], java.lang.String[]) | |
*/ | |
public Object instantiate(String className, Object[] params, | |
String[] signature) throws ReflectionException, MBeanException { | |
return inner.instantiate(className, params, signature); | |
} | |
/** | |
* @param className | |
* @param loaderName | |
* @param params | |
* @param signature | |
* @return | |
* @throws ReflectionException | |
* @throws MBeanException | |
* @throws InstanceNotFoundException | |
* @see javax.management.MBeanServer#instantiate(java.lang.String, javax.management.ObjectName, java.lang.Object[], java.lang.String[]) | |
*/ | |
public Object instantiate(String className, ObjectName loaderName, | |
Object[] params, String[] signature) | |
throws ReflectionException, MBeanException, | |
InstanceNotFoundException { | |
return inner.instantiate(className, loaderName, params, signature); | |
} | |
/** | |
* @param name | |
* @param data | |
* @return | |
* @throws InstanceNotFoundException | |
* @throws OperationsException | |
* @deprecated | |
* @see javax.management.MBeanServer#deserialize(javax.management.ObjectName, byte[]) | |
*/ | |
public ObjectInputStream deserialize(ObjectName name, byte[] data) | |
throws InstanceNotFoundException, OperationsException { | |
return inner.deserialize(name, data); | |
} | |
/** | |
* @param className | |
* @param data | |
* @return | |
* @throws OperationsException | |
* @throws ReflectionException | |
* @deprecated | |
* @see javax.management.MBeanServer#deserialize(java.lang.String, byte[]) | |
*/ | |
public ObjectInputStream deserialize(String className, byte[] data) | |
throws OperationsException, ReflectionException { | |
return inner.deserialize(className, data); | |
} | |
/** | |
* @param className | |
* @param loaderName | |
* @param data | |
* @return | |
* @throws InstanceNotFoundException | |
* @throws OperationsException | |
* @throws ReflectionException | |
* @deprecated | |
* @see javax.management.MBeanServer#deserialize(java.lang.String, javax.management.ObjectName, byte[]) | |
*/ | |
public ObjectInputStream deserialize(String className, | |
ObjectName loaderName, byte[] data) | |
throws InstanceNotFoundException, OperationsException, | |
ReflectionException { | |
return inner.deserialize(className, loaderName, data); | |
} | |
/** | |
* @param mbeanName | |
* @return | |
* @throws InstanceNotFoundException | |
* @see javax.management.MBeanServer#getClassLoaderFor(javax.management.ObjectName) | |
*/ | |
public ClassLoader getClassLoaderFor(ObjectName mbeanName) | |
throws InstanceNotFoundException { | |
return inner.getClassLoaderFor(mbeanName); | |
} | |
/** | |
* @param loaderName | |
* @return | |
* @throws InstanceNotFoundException | |
* @see javax.management.MBeanServer#getClassLoader(javax.management.ObjectName) | |
*/ | |
public ClassLoader getClassLoader(ObjectName loaderName) | |
throws InstanceNotFoundException { | |
return inner.getClassLoader(loaderName); | |
} | |
/** | |
* @return | |
* @see javax.management.MBeanServer#getClassLoaderRepository() | |
*/ | |
public ClassLoaderRepository getClassLoaderRepository() { | |
return inner.getClassLoaderRepository(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment