1/*
2 * Copyright (c) 1999, 2017, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package com.sun.jmx.mbeanserver;
27
28import com.sun.jmx.defaults.ServiceName;
29import static com.sun.jmx.defaults.JmxProperties.MBEANSERVER_LOGGER;
30
31import java.util.ArrayList;
32import java.util.Collections;
33import java.util.HashMap;
34import java.util.HashSet;
35import java.util.List;
36import java.util.concurrent.locks.ReentrantReadWriteLock;
37import java.lang.System.Logger.Level;
38import java.util.Map;
39import java.util.Set;
40import javax.management.DynamicMBean;
41import javax.management.InstanceAlreadyExistsException;
42import javax.management.InstanceNotFoundException;
43import javax.management.ObjectName;
44import javax.management.QueryExp;
45import javax.management.RuntimeOperationsException;
46
47/**
48 * This repository does not support persistency.
49 *
50 * @since 1.5
51 */
52public class Repository {
53
54    /**
55     * An interface that allows the caller to get some control
56     * over the registration.
57     * @see #addMBean
58     * @see #remove
59     */
60    public interface RegistrationContext {
61        /**
62         * Called by {@link #addMBean}.
63         * Can throw a RuntimeOperationsException to cancel the
64         * registration.
65         */
66        public void registering();
67
68        /**
69         * Called by {@link #remove}.
70         * Any exception thrown by this method will be ignored.
71         */
72        public void unregistered();
73    }
74
75    // Private fields -------------------------------------------->
76
77    /**
78     * The structure for storing the objects is very basic.
79     * A Hashtable is used for storing the different domains
80     * For each domain, a hashtable contains the instances with
81     * canonical key property list string as key and named object
82     * aggregated from given object name and mbean instance as value.
83     */
84    private final Map<String,Map<String,NamedObject>> domainTb;
85
86    /**
87     * Number of elements contained in the Repository
88     */
89    private volatile int nbElements = 0;
90
91    /**
92     * Domain name of the server the repository is attached to.
93     * It is quicker to store the information in the repository rather
94     * than querying the framework each time the info is required.
95     */
96    private final String domain;
97
98    /**
99     * We use a global reentrant read write lock to protect the repository.
100     * This seems safer and more efficient: we are using Maps of Maps,
101     * Guaranteing consistency while using Concurent objects at each level
102     * may be more difficult.
103     **/
104    private final ReentrantReadWriteLock lock;
105
106    // Private fields <=============================================
107
108    // Private methods --------------------------------------------->
109
110    /* This class is used to match an ObjectName against a pattern. */
111    private final static class ObjectNamePattern {
112        private final String[] keys;
113        private final String[] values;
114        private final String   properties;
115        private final boolean  isPropertyListPattern;
116        private final boolean  isPropertyValuePattern;
117
118        /**
119         * The ObjectName pattern against which ObjectNames are matched.
120         **/
121        public final ObjectName pattern;
122
123        /**
124         * Builds a new ObjectNamePattern object from an ObjectName pattern.
125         * @param pattern The ObjectName pattern under examination.
126         **/
127        public ObjectNamePattern(ObjectName pattern) {
128            this(pattern.isPropertyListPattern(),
129                 pattern.isPropertyValuePattern(),
130                 pattern.getCanonicalKeyPropertyListString(),
131                 pattern.getKeyPropertyList(),
132                 pattern);
133        }
134
135        /**
136         * Builds a new ObjectNamePattern object from an ObjectName pattern
137         * constituents.
138         * @param propertyListPattern pattern.isPropertyListPattern().
139         * @param propertyValuePattern pattern.isPropertyValuePattern().
140         * @param canonicalProps pattern.getCanonicalKeyPropertyListString().
141         * @param keyPropertyList pattern.getKeyPropertyList().
142         * @param pattern The ObjectName pattern under examination.
143         **/
144        ObjectNamePattern(boolean propertyListPattern,
145                          boolean propertyValuePattern,
146                          String canonicalProps,
147                          Map<String,String> keyPropertyList,
148                          ObjectName pattern) {
149            this.isPropertyListPattern = propertyListPattern;
150            this.isPropertyValuePattern = propertyValuePattern;
151            this.properties = canonicalProps;
152            final int len = keyPropertyList.size();
153            this.keys   = new String[len];
154            this.values = new String[len];
155            int i = 0;
156            for (Map.Entry<String,String> entry : keyPropertyList.entrySet()) {
157                keys[i]   = entry.getKey();
158                values[i] = entry.getValue();
159                i++;
160            }
161            this.pattern = pattern;
162        }
163
164        /**
165         * Return true if the given ObjectName matches the ObjectName pattern
166         * for which this object has been built.
167         * WARNING: domain name is not considered here because it is supposed
168         *          not to be wildcard when called. PropertyList is also
169         *          supposed not to be zero-length.
170         * @param name The ObjectName we want to match against the pattern.
171         * @return true if <code>name</code> matches the pattern.
172         **/
173        public boolean matchKeys(ObjectName name) {
174            // If key property value pattern but not key property list
175            // pattern, then the number of key properties must be equal
176            //
177            if (isPropertyValuePattern &&
178                !isPropertyListPattern &&
179                (name.getKeyPropertyList().size() != keys.length))
180                return false;
181
182            // If key property value pattern or key property list pattern,
183            // then every property inside pattern should exist in name
184            //
185            if (isPropertyValuePattern || isPropertyListPattern) {
186                for (int i = keys.length - 1; i >= 0 ; i--) {
187                    // Find value in given object name for key at current
188                    // index in receiver
189                    //
190                    String v = name.getKeyProperty(keys[i]);
191                    // Did we find a value for this key ?
192                    //
193                    if (v == null) return false;
194                    // If this property is ok (same key, same value), go to next
195                    //
196                    if (isPropertyValuePattern &&
197                        pattern.isPropertyValuePattern(keys[i])) {
198                        // wildmatch key property values
199                        // values[i] is the pattern;
200                        // v is the string
201                        if (Util.wildmatch(v,values[i]))
202                            continue;
203                        else
204                            return false;
205                    }
206                    if (v.equals(values[i])) continue;
207                    return false;
208                }
209                return true;
210            }
211
212            // If no pattern, then canonical names must be equal
213            //
214            final String p1 = name.getCanonicalKeyPropertyListString();
215            final String p2 = properties;
216            return (p1.equals(p2));
217        }
218    }
219
220    /**
221     * Add all the matching objects from the given hashtable in the
222     * result set for the given ObjectNamePattern
223     * Do not check whether the domains match (only check for matching
224     * key property lists - see <i>matchKeys()</i>)
225     **/
226    private void addAllMatching(final Map<String,NamedObject> moiTb,
227                                final Set<NamedObject> result,
228                                final ObjectNamePattern pattern) {
229        synchronized (moiTb) {
230            for (NamedObject no : moiTb.values()) {
231                final ObjectName on = no.getName();
232                // if all couples (property, value) are contained
233                if (pattern.matchKeys(on)) result.add(no);
234            }
235        }
236    }
237
238    private void addNewDomMoi(final DynamicMBean object,
239                              final String dom,
240                              final ObjectName name,
241                              final RegistrationContext context) {
242        final Map<String,NamedObject> moiTb =
243            new HashMap<String,NamedObject>();
244        final String key = name.getCanonicalKeyPropertyListString();
245        addMoiToTb(object,name,key,moiTb,context);
246        domainTb.put(dom, moiTb);
247        nbElements++;
248    }
249
250    private void registering(RegistrationContext context) {
251        if (context == null) return;
252        try {
253            context.registering();
254        } catch (RuntimeOperationsException x) {
255            throw x;
256        } catch (RuntimeException x) {
257            throw new RuntimeOperationsException(x);
258        }
259    }
260
261    private void unregistering(RegistrationContext context, ObjectName name) {
262        if (context == null) return;
263        try {
264            context.unregistered();
265        } catch (Exception x) {
266            // shouldn't come here...
267            MBEANSERVER_LOGGER.log(Level.DEBUG,
268                    "Unexpected exception while unregistering "+name,
269                    x);
270        }
271    }
272
273    private void addMoiToTb(final DynamicMBean object,
274            final ObjectName name,
275            final String key,
276            final Map<String,NamedObject> moiTb,
277            final RegistrationContext context) {
278        registering(context);
279        moiTb.put(key,new NamedObject(name, object));
280    }
281
282    /**
283     * Retrieves the named object contained in repository
284     * from the given objectname.
285     */
286    private NamedObject retrieveNamedObject(ObjectName name) {
287
288        // No patterns inside reposit
289        if (name.isPattern()) return null;
290
291        // Extract the domain name.
292        String dom = name.getDomain().intern();
293
294        // Default domain case
295        if (dom.length() == 0) {
296            dom = domain;
297        }
298
299        Map<String,NamedObject> moiTb = domainTb.get(dom);
300        if (moiTb == null) {
301            return null; // No domain containing registered object names
302        }
303
304        return moiTb.get(name.getCanonicalKeyPropertyListString());
305    }
306
307    // Private methods <=============================================
308
309    // Protected methods --------------------------------------------->
310
311    // Protected methods <=============================================
312
313    // Public methods --------------------------------------------->
314
315    /**
316     * Construct a new repository with the given default domain.
317     */
318    public Repository(String domain) {
319        this(domain,true);
320    }
321
322    /**
323     * Construct a new repository with the given default domain.
324     */
325    public Repository(String domain, boolean fairLock) {
326        lock = new ReentrantReadWriteLock(fairLock);
327
328        domainTb = new HashMap<String,Map<String,NamedObject>>(5);
329
330        if (domain != null && domain.length() != 0)
331            this.domain = domain.intern(); // we use == domain later on...
332        else
333            this.domain = ServiceName.DOMAIN;
334
335        // Creates a new hashtable for the default domain
336        domainTb.put(this.domain, new HashMap<String,NamedObject>());
337    }
338
339    /**
340     * Returns the list of domains in which any MBean is currently
341     * registered.
342     *
343     */
344    public String[] getDomains() {
345
346        lock.readLock().lock();
347        final List<String> result;
348        try {
349            // Temporary list
350            result = new ArrayList<String>(domainTb.size());
351            for (Map.Entry<String,Map<String,NamedObject>> entry :
352                     domainTb.entrySet()) {
353                // Skip domains that are in the table but have no
354                // MBean registered in them
355                // in particular the default domain may be like this
356                Map<String,NamedObject> t = entry.getValue();
357                if (t != null && t.size() != 0)
358                    result.add(entry.getKey());
359            }
360        } finally {
361            lock.readLock().unlock();
362        }
363
364        // Make an array from result.
365        return result.toArray(new String[result.size()]);
366    }
367
368    /**
369     * Stores an MBean associated with its object name in the repository.
370     *
371     * @param object  MBean to be stored in the repository.
372     * @param name    MBean object name.
373     * @param context A registration context. If non null, the repository
374     *                will call {@link RegistrationContext#registering()
375     *                context.registering()} from within the repository
376     *                lock, when it has determined that the {@code object}
377     *                can be stored in the repository with that {@code name}.
378     *                If {@link RegistrationContext#registering()
379     *                context.registering()} throws an exception, the
380     *                operation is abandonned, the MBean is not added to the
381     *                repository, and a {@link RuntimeOperationsException}
382     *                is thrown.
383     */
384    public void addMBean(final DynamicMBean object, ObjectName name,
385            final RegistrationContext context)
386        throws InstanceAlreadyExistsException {
387
388        if (MBEANSERVER_LOGGER.isLoggable(Level.TRACE)) {
389            MBEANSERVER_LOGGER.log(Level.TRACE, "name = " + name);
390        }
391
392        // Extract the domain name.
393        String dom = name.getDomain().intern();
394        boolean to_default_domain = false;
395
396        // Set domain to default if domain is empty and not already set
397        if (dom.length() == 0)
398            name = Util.newObjectName(domain + name.toString());
399
400        // Do we have default domain ?
401        if (dom == domain) {  // ES: OK (dom & domain are interned)
402            to_default_domain = true;
403            dom = domain;
404        } else {
405            to_default_domain = false;
406        }
407
408        // Validate name for an object
409        if (name.isPattern()) {
410            throw new RuntimeOperationsException(
411             new IllegalArgumentException("Repository: cannot add mbean for " +
412                                          "pattern name " + name.toString()));
413        }
414
415        lock.writeLock().lock();
416        try {
417            // Domain cannot be JMImplementation if entry does not exist
418            if ( !to_default_domain &&
419                    dom.equals("JMImplementation") &&
420                    domainTb.containsKey("JMImplementation")) {
421                throw new RuntimeOperationsException(
422                        new IllegalArgumentException(
423                        "Repository: domain name cannot be JMImplementation"));
424            }
425
426            // If domain does not already exist, add it to the hash table
427            final Map<String,NamedObject> moiTb = domainTb.get(dom);
428            if (moiTb == null) {
429                addNewDomMoi(object, dom, name, context);
430                return;
431            } else {
432                // Add instance if not already present
433                String cstr = name.getCanonicalKeyPropertyListString();
434                NamedObject elmt= moiTb.get(cstr);
435                if (elmt != null) {
436                    throw new InstanceAlreadyExistsException(name.toString());
437                } else {
438                    nbElements++;
439                    addMoiToTb(object,name,cstr,moiTb,context);
440                }
441            }
442
443        } finally {
444            lock.writeLock().unlock();
445        }
446    }
447
448    /**
449     * Checks whether an MBean of the name specified is already stored in
450     * the repository.
451     *
452     * @param name name of the MBean to find.
453     *
454     * @return  true if the MBean is stored in the repository,
455     *          false otherwise.
456     */
457    public boolean contains(ObjectName name) {
458        if (MBEANSERVER_LOGGER.isLoggable(Level.TRACE)) {
459            MBEANSERVER_LOGGER.log(Level.TRACE, "name = " + name);
460        }
461        lock.readLock().lock();
462        try {
463            return (retrieveNamedObject(name) != null);
464        } finally {
465            lock.readLock().unlock();
466        }
467    }
468
469    /**
470     * Retrieves the MBean of the name specified from the repository. The
471     * object name must match exactly.
472     *
473     * @param name name of the MBean to retrieve.
474     *
475     * @return  The retrieved MBean if it is contained in the repository,
476     *          null otherwise.
477     */
478    public DynamicMBean retrieve(ObjectName name) {
479        if (MBEANSERVER_LOGGER.isLoggable(Level.TRACE)) {
480            MBEANSERVER_LOGGER.log(Level.TRACE, "name = " + name);
481        }
482
483        // Calls internal retrieve method to get the named object
484        lock.readLock().lock();
485        try {
486            NamedObject no = retrieveNamedObject(name);
487            if (no == null) return null;
488            else return no.getObject();
489        } finally {
490            lock.readLock().unlock();
491        }
492    }
493
494    /**
495     * Selects and retrieves the list of MBeans whose names match the specified
496     * object name pattern and which match the specified query expression
497     * (optionally).
498     *
499     * @param pattern The name of the MBean(s) to retrieve - may be a specific
500     * object or a name pattern allowing multiple MBeans to be selected.
501     * @param query query expression to apply when selecting objects - this
502     * parameter will be ignored when the Repository Service does not
503     * support filtering.
504     *
505     * @return  The list of MBeans selected. There may be zero, one or many
506     *          MBeans returned in the set.
507     */
508    public Set<NamedObject> query(ObjectName pattern, QueryExp query) {
509
510        final Set<NamedObject> result = new HashSet<NamedObject>();
511
512        // The following filter cases are considered:
513        // null, "", "*:*" : names in all domains
514        // ":*", ":[key=value],*" : names in defaultDomain
515        // "domain:*", "domain:[key=value],*" : names in the specified domain
516
517        // Surely one of the most frequent cases ... query on the whole world
518        ObjectName name;
519        if (pattern == null ||
520            pattern.getCanonicalName().length() == 0 ||
521            pattern.equals(ObjectName.WILDCARD))
522           name = ObjectName.WILDCARD;
523        else name = pattern;
524
525        lock.readLock().lock();
526        try {
527
528            // If pattern is not a pattern, retrieve this mbean !
529            if (!name.isPattern()) {
530                final NamedObject no = retrieveNamedObject(name);
531                if (no != null) result.add(no);
532                return result;
533            }
534
535            // All names in all domains
536            if (name == ObjectName.WILDCARD) {
537                for (Map<String,NamedObject> moiTb : domainTb.values()) {
538                    result.addAll(moiTb.values());
539                }
540                return result;
541            }
542
543            final String canonical_key_property_list_string =
544                    name.getCanonicalKeyPropertyListString();
545            final boolean allNames =
546                    (canonical_key_property_list_string.length()==0);
547            final ObjectNamePattern namePattern =
548                (allNames?null:new ObjectNamePattern(name));
549
550            // All names in default domain
551            if (name.getDomain().length() == 0) {
552                final Map<String,NamedObject> moiTb = domainTb.get(domain);
553                if (allNames)
554                    result.addAll(moiTb.values());
555                else
556                    addAllMatching(moiTb, result, namePattern);
557                return result;
558            }
559
560            if (!name.isDomainPattern()) {
561                final Map<String,NamedObject> moiTb = domainTb.get(name.getDomain());
562                if (moiTb == null) return Collections.emptySet();
563                if (allNames)
564                    result.addAll(moiTb.values());
565                else
566                    addAllMatching(moiTb, result, namePattern);
567                return result;
568            }
569
570            // Pattern matching in the domain name (*, ?)
571            final String dom2Match = name.getDomain();
572            for (String dom : domainTb.keySet()) {
573                if (Util.wildmatch(dom, dom2Match)) {
574                    final Map<String,NamedObject> moiTb = domainTb.get(dom);
575                    if (allNames)
576                        result.addAll(moiTb.values());
577                    else
578                        addAllMatching(moiTb, result, namePattern);
579                }
580            }
581            return result;
582        } finally {
583            lock.readLock().unlock();
584        }
585    }
586
587    /**
588     * Removes an MBean from the repository.
589     *
590     * @param name name of the MBean to remove.
591     * @param context A registration context. If non null, the repository
592     *                will call {@link RegistrationContext#unregistered()
593     *                context.unregistered()} from within the repository
594     *                lock, just after the mbean associated with
595     *                {@code name} is removed from the repository.
596     *                If {@link RegistrationContext#unregistered()
597     *                context.unregistered()} is not expected to throw any
598     *                exception. If it does, the exception is logged
599     *                and swallowed.
600     *
601     * @exception InstanceNotFoundException The MBean does not exist in
602     *            the repository.
603     */
604    public void remove(final ObjectName name,
605            final RegistrationContext context)
606        throws InstanceNotFoundException {
607
608        // Debugging stuff
609        if (MBEANSERVER_LOGGER.isLoggable(Level.TRACE)) {
610            MBEANSERVER_LOGGER.log(Level.TRACE, "name = " + name);
611        }
612
613        // Extract domain name.
614        String dom= name.getDomain().intern();
615
616        // Default domain case
617        if (dom.length() == 0) dom = domain;
618
619        lock.writeLock().lock();
620        try {
621            // Find the domain subtable
622            final Map<String,NamedObject> moiTb = domainTb.get(dom);
623            if (moiTb == null) {
624                throw new InstanceNotFoundException(name.toString());
625            }
626
627            // Remove the corresponding element
628            if (moiTb.remove(name.getCanonicalKeyPropertyListString())==null) {
629                throw new InstanceNotFoundException(name.toString());
630            }
631
632            // We removed it !
633            nbElements--;
634
635            // No more object for this domain, we remove this domain hashtable
636            if (moiTb.isEmpty()) {
637                domainTb.remove(dom);
638
639                // set a new default domain table (always present)
640                // need to reinstantiate a hashtable because of possible
641                // big buckets array size inside table, never cleared,
642                // thus the new !
643                if (dom == domain) // ES: OK dom and domain are interned.
644                    domainTb.put(domain, new HashMap<String,NamedObject>());
645            }
646
647            unregistering(context,name);
648
649        } finally {
650            lock.writeLock().unlock();
651        }
652    }
653
654    /**
655     * Gets the number of MBeans stored in the repository.
656     *
657     * @return  Number of MBeans.
658     */
659    public Integer getCount() {
660        return nbElements;
661    }
662
663    /**
664     * Gets the name of the domain currently used by default in the
665     * repository.
666     *
667     * @return  A string giving the name of the default domain name.
668     */
669    public String getDefaultDomain() {
670        return domain;
671    }
672
673    // Public methods <=============================================
674}
675