/*
 * ProductCodes.java
 * 
 * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - Neither the name of Sun Microsystems nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package example.productcodes;

import java.lang.management.ManagementFactory;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.management.JMX;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeDataView;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;
import javax.management.openmbean.TabularData;
import javax.management.openmbean.TabularDataSupport;
import javax.management.openmbean.TabularType;

/**
 * The ProductCodes class to illustrate our example.
 * Run 'java example.productcodes.ProductCodes', then connect to the 
 * application using JConsole or 
 * VisualVM.
 * 
 * <p><i>Note: The purpose of the code shown here is to illustrate an
 *       example discussed on the author's weblog.</i></p>
 * @author Daniel Fuchs
 */
public class ProductCodes {
    
    // helper methods
    static <K, V> Map<K, V> newHashMap() {
        return new HashMap<K, V>();
    }
    static <K, V> Map<K, V> newHashMap(Map<? extends K, ? extends V> map) {
        return new HashMap<K, V>(map);
    }
    static <K, V> Map<K, V> newHashMap(int initialCapacity) {
        return new HashMap<K, V>(initialCapacity);
    }

    /**
     * This interface defines a registered product, known by its name and its
     * product ID.
     */
    public static interface RegisteredProduct {
        public String getProductName();
        public int    getProductId();
    }

    /**
     * The ProductsDBData interface is a common abstraction for our two 
     * custom mappings discussed in methods 4 and 5 in our ProductsDB MXBean.
     */
    public static interface ProductsDBData {
        /**
         * The time stamp at which the product data was taken.
         */
        public Date getTimeStamp();
        /**
         * A {@code Map<productName,productId>}.
         */
        public Map<String, Integer> toProductsMap();

    }

    /**
     * A first implementation of ProductsDBData, which expects one field per
     * product in the composite data. We cannot use directly the ProductsDBData
     * interface because we need a static 'from' method.
     * The type exported by the MXBean needs therefore to be 
     * 'ProductsDBRecord' - not 'ProductsDBData'.
     */
    public static abstract class ProductsDBRecord implements ProductsDBData {
        public abstract Date getTimeStamp();
        public abstract Map<String, Integer> toProductsMap();
        public static ProductsDBRecord from(CompositeData data) {
            return ProductsDB.ProductsDBRecordImpl.from(data);
        }
    }

    /**
     * A second implementation of ProductsDBData, which uses a custom
     * TabularData containing one row per product. We cannot use directly 
     * the ProductsDBData interface because we need a static 'from' method.  
     * The type exported by the MXBean needs therefore to be 
     * 'ProductsDBRecord' - not 'ProductsDBData'.
     */
    public static abstract class ProductsDBTable implements ProductsDBData {
        public abstract Date getTimeStamp();
        public abstract TabularData getProductsTable();
        public abstract Map<String, Integer> toProductsMap();
        public static ProductsDBTable from(CompositeData data) {
            return ProductsDB.ProductsDBTableImpl.from(data);
        }
    }
        
    /**
     * The interface ProductsDBMXBean defines five different ways of exporting
     * the same information. It demonstrate various custom and default 
     * mappings to open types. 
     */
    public static interface ProductsDBMXBean {

        /**
         * Get product identities - demonstrate regular MXBean mapping
         * to array of CompositeData. 
         * This is usually the easiest way to proceed.
         * Notice how this attribute is displayed by VisualVM/JConsole.
         */
        public RegisteredProduct[] getRegisteredProducts()
                throws InterruptedException;

        /**
         * Get product map - demonstrate regular MXBean mapping
         * to TabularData. An alternative to method 1 above.
         * Notice how this attribute is displayed by VisualVM/JConsole.
         * Often method 1 above gives better results.
         */
        public Map<String, Integer> getProductsMap()
                throws InterruptedException;

        /**
         * Get product map - demonstrate regular MXBean mapping
         * to TabularData, where the value is a CompositeData and the 
         * key is duplicated. An alternative to method 1 and 2, usually
         * used when the value (here RegisteredProduct) doesn't contain the
         * key, and/or has more than one field besides the key.
         * A bit redundant in our context where RegisteredProduct has only
         * one field besides the key - but shown for the sake of the example.
         * Notice how this attribute is displayed by VisualVM/JConsole.
         */
        public Map<String, RegisteredProduct> getRegisteredProductsMap()
                throws InterruptedException;

        /**
         * Get a record of all products - a custom mapping - usually discouraged
         * unless absolutely necessary.
         */
        public ProductsDBRecord getProductsDBRecord()
                throws InterruptedException, OpenDataException;

        /**
         * Get a record of all products - a custom mapping - usually discouraged
         * unless absolutely necessary. Notice how the TabularData displayed
         * in JConsole/VisualVM has row with meaningful fields names - similar
         * to method 1 (RegisteredProduct[]) and better than for method 2  
         * method 2 (Map<String,Integer>)
         */
        public ProductsDBTable getProductsDBTable()
                throws InterruptedException, OpenDataException;

        /**
         * register a new product
         * @param productName product name
         * @return the registered product id, or null.
         */
        public int registerProduct(String productName)
                throws InterruptedException;

        /**
         * remove a registered product
         * @param productName product name
         * @return the removed product id.
         * @throws IllegalArgumentException if the product is not registered.
         */
        public int removeProduct(String productName)
                throws InterruptedException;
    }

    /**
     * This is our actual productsDB MXBean implementation.
     * This class demonstrate various possibilities to expose a list of 
     * registered products - composed of 
     * {@code String: productName, Integer: productId} pairs, 
     * in the attributes of an MXBean.
     */
    public static class ProductsDB implements ProductsDBMXBean {

        private final Map<String, Integer> productMap;
        private final ReadWriteLock lock;
        private int productIdGenerator = 0;

        // Constants used to implement our custom MXBeans mappings.
        private static final class Fields {
            public static final String productName  = "productName";
            public static final String productId    = "productId";
            public static final String timeStamp    = "timeStamp";
            public static final String productTable = "productTable";
        }

        public ProductsDB() {
            productMap = newHashMap();
            lock = new ReentrantReadWriteLock();
        }

        /**
         * Regular mapping - Method 1: return an array of interfaces.
         * This attribute will be mapped to a CompositeData[] array.
         * Each composite data will have two fields - corresponding to the
         * two getters defined in the RegisteredProduct interface...
         * Use JConsole or VisualVM to see how this attribute is displayed.
         */
        public RegisteredProduct[] getRegisteredProducts()
                throws InterruptedException {
            if (!lock.readLock().tryLock(3, TimeUnit.SECONDS)) {
                throw new InterruptedException("Resource locked. Retry later.");
            }
            try {
                final RegisteredProduct[] registrations =
                        new RegisteredProduct[productMap.size()];
                int count = 0;
                for (Map.Entry<String, Integer> product:productMap.entrySet()) {
                    final String productName = product.getKey();
                    final int productId = product.getValue();
                    final RegisteredProduct 
                            registered = new RegisteredProduct() {
                        public String getProductName() {return productName;}
                        public int getProductId() {return productId;}
                    };
                    registrations[count++] = registered;
                }
                return registrations;
            } finally {
                lock.readLock().unlock();
            }
        }

        /**
         * Regular mapping - Method 2: return a {@code Map<String,Integer>}
         * This attribute will be mapped to a TabularData. 
         * Each row of the tabular data will have two fieldss, one called 'key'
         * whose value will be the product productName, one called 'value' 
         * whose value will be the product productId.
         * Use JConsole or VisualVM to see how this attribute is displayed.
         */
        public Map<String, Integer> getProductsMap() 
                throws InterruptedException {
            if (!lock.readLock().tryLock(3, TimeUnit.SECONDS)) {
                throw new InterruptedException("Resource locked. Retry later.");
            }
            try {
                return Collections.unmodifiableMap(productMap);
            } finally {
                lock.readLock().unlock();
            }
        }

        /**
         * Regular mapping - Method 3: return a 
         * {@code Map<String,RegisteredProduct>}
         * This attribute will be mapped to a TabularData. 
         * Each row of the tabular data will have two fields, one called 'key'
         * whose value will be the product productName, one called 'value' 
         * whose value will be the a composite data containing two fields, 
         * one called 'productName' whose value will be the same than the 
         * value of 'key', one called 'productId' whose value will be the 
         * product productId.
         * This is a bit redundant, and does not offer any advantage compared
         * to method 1, so in general method 1 (array of RegisteredProduct) 
         * will be prefered.
         * Use JConsole or VisualVM to see how this attribute is displayed.
         */
        public Map<String, RegisteredProduct> getRegisteredProductsMap()
                throws InterruptedException {
            if (!lock.readLock().tryLock(3, TimeUnit.SECONDS)) {
                throw new InterruptedException("Resource locked. Retry later.");
            }
            try {
                final RegisteredProduct[] uids = getRegisteredProducts();
                final Map<String, RegisteredProduct> map =
                        newHashMap(uids.length);
                for (RegisteredProduct u : uids) {
                    map.put(u.getProductName(), u);
                }
                return map;
            } finally {
                lock.readLock().unlock();
            }
        }

        /**
         * Custom mapping - Method 4:
         * Return a CompositeData where each field correspond to a different 
         * product.
         * The productName of the field is the productName of the product.
         * This cannot be achieved directly - so we're taking a round-about.
         * We cannot directly return CompositeData, because CompositeDatas are 
         * not recognized by the MXBean framework. The MXBean framework expects
         * all attributes of the MXBean to be custom (or simple) types.
         * It doesn't expect the MXBean to directly return CompositeData.
         * To work around this, we're defining a class - ProductsDBRecord, 
         * which we will map ourselves to a composite data using the 
         * CompositeDataView interface. However - ProductsDBRecord needs to 
         * have at least one mappable field to be accepted by the MXBean 
         * framework. We therefore define a class which contains a single field,
         * "timeStamp". This will be mapped onto a CompositeData that contains
         * a single "timeStamp" field. At runtime we're going to extend the 
         * actual value of the composite data returned in order to add 
         * additional fields - one per product.
         * To do this, we return a subclass of ProductsDBRecord which 
         * implements CompositeDataView. 
         * Since we want ProductsDBRecord to be also meaningful for a regular
         * client using MXBean proxies, we also need to add some methods that
         * such a client might call to retrieve the actual information, that is,
         * our {@code Map<productName,productId>}. To do that, we define a 
         * {@code toProductsMap()} method in ProductsDBRecord. We use 'to' 
         * rather than 'get' because we don't want the returned Map to be 
         * mapped onto yet another composite data field. 
         * We also need to define a 'from(CompositeData)' in the 
         * ProductsDBRecord class, so that an instance of ProductsDBRecord 
         * can be reconstructed on the client side by the MXBean proxy... 
         * 
         * @return A ProductsDBRecord for which we provide our own custom 
         *         CompositeData mapping.
         * @throws java.lang.InterruptedException
         * @throws javax.management.openmbean.OpenDataException
         */
        public ProductsDBRecord getProductsDBRecord()
                throws InterruptedException, OpenDataException {
            if (!lock.readLock().tryLock(3, TimeUnit.SECONDS)) {
                throw new InterruptedException("Resource locked. Retry later.");
            }
            try {
                return new ProductsDBRecordImpl(productMap);
            } finally {
                lock.readLock().unlock();
            }
        }

        /**
         * Custom Mapping - Method 5: 
         * Return a tabular data where each row is composed of two fields:
         * one is the productName, the other is the productId.
         * Like for the Method 4 - we cannot do this directly and use the same
         * kind of round-about - which is also a bit contrieved.
         * Since there is no such thing as a 'TabularDataView' we create a 
         * custom class - ProductsDBTable, which we will map to a CompositeData
         * using CompositeDataView. 
         * Our CompositeData will contain two fields: one is a timeStamp, the 
         * other is our custom tabular data.
         * 
         * @return A ProductsDBTable for which we provide our own custom 
         *         CompositeData mapping.
         * @throws java.lang.InterruptedException
         * @throws javax.management.openmbean.OpenDataException
         */
        public ProductsDBTable getProductsDBTable()
                throws InterruptedException, OpenDataException {
            if (!lock.readLock().tryLock(3, TimeUnit.SECONDS)) {
                throw new InterruptedException("Resource locked. Retry later.");
            }
            try {
                return new ProductsDBTableImpl(productMap);
            } finally {
                lock.readLock().unlock();
            }

        }

        /**
         * A class that implements custom CompositeData mapping for 
         * method 4 above...
         */
        public static class ProductsDBRecordImpl extends ProductsDBRecord
                implements CompositeDataView {

            private final Date timeStamp;
            private final Map<String, Integer> productMap;

            private ProductsDBRecordImpl(Map<String, Integer> productMap) {
                this(productMap, new Date());
            }

            private ProductsDBRecordImpl(Map<String, Integer> productMap, 
                    Date date) {
                this.productMap = newHashMap(productMap);
                this.timeStamp = date;
            }

            public Date getTimeStamp() {
                return timeStamp;
            }

            public Map<String, Integer> toProductsMap() {
                return productMap;
            }

            /**
             * The toCompositeData() method will extend the 'regular' mapping
             * of ProductsDBRecord, adding one field per product to returned 
             * composite data.
             * 
             * @return An instance of CompositeDataSupport. 
             *         For interoperability with generic clients such as 
             *         JConsole and VisualVM, it is very important to return 
             *         a CompositeDataSupport, and not a subclass.
             */
            public CompositeData toCompositeData(CompositeType ct) {
                try {
                    // Creates a map for composite data values.
                    // It will contain 1 field for each products, plus 
                    final Map<String, Object> values =
                            newHashMap();
                    values.putAll(productMap);
                    values.put(Fields.timeStamp, getTimeStamp());
                    final String[] productNames =
                            productMap.keySet().
                            toArray(new String[productMap.size()]);
                    final String[] itemNames = 
                            new String[productNames.length + 1];
                    System.arraycopy(productNames, 0, itemNames, 1,
                            productNames.length);
                    itemNames[0] = Fields.timeStamp;
                    final OpenType[] itemTypes = 
                            new OpenType[productMap.size() + 1];
                    Arrays.fill(itemTypes, SimpleType.INTEGER);
                    itemTypes[0] = SimpleType.DATE;
                    final CompositeType type =
                            new CompositeType(
                            ct.getTypeName(),
                            ct.getDescription(),
                            itemNames, itemNames, itemTypes);
                    return new CompositeDataSupport(type, values);
                } catch (RuntimeException x) {
                    throw x;
                } catch (Exception x) {
                    throw new IllegalArgumentException(ct.getTypeName(), x);
                }
            }

            @Override
            public String toString() {
                return "{" + Fields.timeStamp + "=" + 
                        String.valueOf(timeStamp) +
                        ", " + Fields.productTable + "=" +
                        String.valueOf(productMap) + "}";
            }

            /**
             * The from() method makes it possible to reconstruct an instance
             * of ProductsDBRecordImpl from the received CompositeData on the
             * client side. You only need to implement that if you want to be
             * able to use MXBeans proxies on the client side.
             */
            public static ProductsDBRecordImpl from(CompositeData data) {
                final Date date = (Date) data.get(Fields.timeStamp);
                Map<String, Integer> products = newHashMap();
                for (String key : data.getCompositeType().keySet()) {
                    if (key.equals(Fields.timeStamp)) {
                        continue;
                    }
                    products.put(key, (Integer) data.get(key));
                }
                return new ProductsDBRecordImpl(products, date);
            }
        }

        /**
         * A class that implements custom CompositeData / TabularDat mapping 
         * for method 5 above...
         */
        public static class ProductsDBTableImpl extends ProductsDBTable
                implements CompositeDataView {

            private final Date timeStamp;
            private final ProductsTable productTable;

            private ProductsDBTableImpl(Map<String, Integer> productMap) {
                this(productMap, new Date());
            }

            private ProductsDBTableImpl(Map<String, Integer> productMap, 
                    Date timeStamp) {
                this.productTable = new ProductsTable(productMap);
                this.timeStamp = timeStamp;
            }

            @Override
            public Date getTimeStamp() {
                return timeStamp;
            }

            @Override
            public TabularData getProductsTable() {
                return productTable.toTabularData();
            }

            @Override
            public Map<String, Integer> toProductsMap() {
                return productTable.toProductsMap();
            }

            @Override
            public String toString() {
                return "{" + Fields.timeStamp + "=" + 
                        String.valueOf(timeStamp) +
                        ", " + Fields.productTable + "=" +
                        String.valueOf(productTable) + "}";
            }

            /**
             * The toCompositeData() method will extend the 'regular' mapping
             * of ProductsDBTable, providing a meaningful content for the 
             * TabularData returned by getProductsTable(). 
             * 
             * @return An instance of CompositeDataSupport. 
             *         For interoperability with generic clients such as 
             *         JConsole and VisualVM, it is very important to return 
             *         a CompositeDataSupport, and not a subclass.
             */
            public CompositeData toCompositeData(CompositeType ct) {
                try {
                    final TabularData table = productTable.toTabularData();
                    final String[] itemNames = {
                        Fields.timeStamp, Fields.productTable
                    };
                    // replace the generated TabularType by our own...
                    final OpenType[] itemTypes = {SimpleType.DATE,
                        table.getTabularType()
                    };
                    final CompositeType type =
                            new CompositeType(// this.getClass().getName(), 
                            ct.getTypeName(),
                            ct.getDescription(),
                            itemNames, itemNames, itemTypes);
                    // provide our own values for timeStamp and productTable.
                    final Object[] itemValues = {timeStamp, table};
                    return new CompositeDataSupport(type,itemNames,itemValues);
                } catch (RuntimeException x) {
                    throw x;
                } catch (Exception x) {
                    throw new IllegalArgumentException(ct.getTypeName(), x);
                }
            }

            /**
             * The from() method makes it possible to reconstruct an instance
             * of ProductsDBTableImpl from the received CompositeData on the
             * client side. You only need to implement that if you want to be
             * able to use MXBeans proxies on the client side.
             */
            public static ProductsDBTableImpl from(CompositeData data) {
                final Date date = (Date) data.get(Fields.timeStamp);
                Map<String, Integer> products = newHashMap();
                final TabularData productTable =
                        (TabularData) data.get(Fields.productTable);
                for (CompositeData val : 
                     (Collection<CompositeData>) productTable.values()) {
                    products.put((String) val.get(Fields.productName),
                            (Integer) val.get(Fields.productId));
                }
                return new ProductsDBTableImpl(products, date);
            }
        }

        /**
         * A helper class that perform a custom mapping to and from
         * Map<productName,productId> from and to a custom TabularData.
         * This makes it possible to have a TabularData whose rows have fields
         * named 'productName' and 'productId' instead of 'key' and 'values' as
         * would have been generated by default (compare with result of 
         * Method 2 and Method 3 above...)
         */
        private static class ProductsTable {

            private static volatile CompositeType rowType = null;
            private static volatile TabularType tabularType = null;
            private final TabularDataSupport table;

            public ProductsTable(Map<String, Integer> productMap) {
                table = new TabularDataSupport(getProductsTableTabularType());
                for (Map.Entry<String, Integer> e : productMap.entrySet()) {
                    table.put(newProductRow(e.getKey(), e.getValue()));
                }
            }

            /**
             * Returns a TabularData view of our products table.
             * 
             * @return An instance of TabularDataSupport. 
             *         For interoperability with generic clients such as 
             *         JConsole and VisualVM, it is very important to return 
             *         a TabularDataSupport, and not a subclass.
             **/
            public TabularData toTabularData() {
                return table;
            }

            /**
             * Returns a {@code Map<productName,productId>} view of our 
             * products table.
             **/
            public Map<String, Integer> toProductsMap() {
                final Map<String, Integer> map = newHashMap();
                for (Object d : table.values()) {
                    final CompositeData cd = (CompositeData) d;
                    map.put((String) cd.get(Fields.productName),
                            (Integer) cd.get(Fields.productId));
                }
                return map;
            }

            /**
             * Construct a new row for our TabularData.
             * @return An instance of CompositeDataSupport. 
             *         For interoperability with generic clients such as 
             *         JConsole and VisualVM, it is very important to return 
             *         a CompositeDataSupport, and not a subclass.
             */
            private CompositeData newProductRow(String productName, 
                    int productId) {
                try {
                    final String[] itemNames = {
                        Fields.productName, Fields.productId
                    };
                    return new CompositeDataSupport(getProductsTableRowType(),
                            itemNames, new Object[]{productName, productId});
                } catch (OpenDataException x) {
                    throw new IllegalArgumentException(
                            "Cannot construct ProductsTable", x);
                }
            }

            @Override
            public String toString() {
                StringBuilder b = new StringBuilder().append("{");
                String start = "{";
                for (Object d : table.values()) {
                    b.append(start).
                            append(Fields.productName).append("=").
                            append(((CompositeData) d).get(Fields.productName)).
                            append(", ").
                            append(Fields.productId).append("=").
                            append(((CompositeData) d).get(Fields.productId)).
                            append("}");
                    start = ", {";
                }
                return b.append("}").toString();
            }
            
            /**
             * Creates and return the CompositeType describing the rows of
             * our custom TabularData.
             **/
            private static CompositeType getProductsTableRowType() {
                if (rowType != null) {
                    return rowType;
                }
                try {
                    final String[] itemNames = {
                        Fields.productName, Fields.productId
                    };
                    final OpenType[] itemTypes = 
                        {SimpleType.STRING, SimpleType.INTEGER};
                    rowType = new CompositeType(
                            RegisteredProduct.class.getName(),
                            "registered product record", itemNames, 
                            itemNames, itemTypes);
                    return rowType;
                } catch (OpenDataException x) {
                    // should not happen.
                    throw new IllegalArgumentException(
                            "Cannot construct ProductsTable", x);
                }
            }

            /**
             * Creates and return the TabularType describing 
             * our custom TabularData.
             */
            private static TabularType getProductsTableTabularType() {
                if (tabularType != null) {
                    return tabularType;
                }
                try {
                    tabularType = new TabularType(ProductsTable.class.getName(),
                            "product table", getProductsTableRowType(),
                            new String[]{Fields.productName});
                    return tabularType;
                } catch (OpenDataException x) {
                    // should not happen.
                    throw new IllegalArgumentException(
                            "Cannot construct ProductsTable", x);
                }
            }
        }

        /**
         * This method will add a new product to the list.
         * @param productName product productNam
         */
        public int registerProduct(String productName) 
                throws InterruptedException {
            if (!lock.writeLock().tryLock(3, TimeUnit.SECONDS)) {
                throw new InterruptedException("Resource locked. Retry later.");
            }
            try {
                if (productMap.containsKey(productName)) {
                    throw new IllegalArgumentException(productName+
                            " already exists");
                }
                productMap.put(productName, productIdGenerator++);
                return productMap.get(productName);
            } finally {
                lock.writeLock().unlock();
            }
        }

        /**
         * This method will remove an existing product from the list.
         * @param productName product productName
         */
        public int removeProduct(String productName)
                throws InterruptedException {
            if (!lock.writeLock().tryLock(3, TimeUnit.SECONDS)) {
                throw new InterruptedException("Resource locked. Retry later.");
            }
            try {
                if (!productMap.containsKey(productName)) {
                    throw new IllegalArgumentException(
                            "product "+productName+" does not exists");
                }
                return productMap.remove(productName);
            } finally {
                lock.writeLock().unlock();
            }
        }
    }

    /**
     * Our main entry points.
     * Creates and registers a ProductsDBMXBean in the platform MBeanServer.
     * Adds to products to the DB: "foo" and "bar".
     * Then waits, so that we can connect with JConsole or VisualVM and examine
     * the differences in the mapping obtained with methods 1, 2, 3, 4, and 5.
     */
    public static void main(String[] args) throws Exception {
        final MBeanServer platform = 
            ManagementFactory.getPlatformMBeanServer();
        final ObjectName mbeanName = new ObjectName("example:type=ProductsDB");
        platform.registerMBean(new ProductsDB(), mbeanName);
        ProductsDBMXBean productsDB =
                JMX.newMXBeanProxy(platform, mbeanName, ProductsDBMXBean.class);
        productsDB.registerProduct("foo");
        productsDB.registerProduct("bar");
        System.out.println("Got products DB record: "+
                productsDB.getProductsDBRecord());
        System.out.println("Got products DB table: "+
                productsDB.getProductsDBTable());
        System.out.println("Strike 'Enter' to exit... ");
        System.in.read();
    }

}