/*
 * Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.jmx.remote.internal;

import com.sun.jmx.remote.security.NotificationAccessController;
import com.sun.jmx.remote.util.ClassLogger;
import com.sun.jmx.remote.util.EnvHelp;
import java.io.IOException;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.management.InstanceNotFoundException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanPermission;
import javax.management.MBeanServer;
import javax.management.MBeanServerDelegate;
import javax.management.MBeanServerNotification;
import javax.management.Notification;
import javax.management.NotificationBroadcaster;
import javax.management.NotificationFilter;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.remote.NotificationResult;
import javax.management.remote.TargetedNotification;
import javax.management.MalformedObjectNameException;
import javax.security.auth.Subject;

public class ServerNotifForwarder {


    public ServerNotifForwarder(MBeanServer mbeanServer,
                                Map<String, ?> env,
                                NotificationBuffer notifBuffer,
                                String connectionId) {
        this.mbeanServer = mbeanServer;
        this.notifBuffer = notifBuffer;
        this.connectionId = connectionId;
        connectionTimeout = EnvHelp.getServerConnectionTimeout(env);

        String stringBoolean = (String) env.get("jmx.remote.x.check.notification.emission");
        checkNotificationEmission = EnvHelp.computeBooleanFromString( stringBoolean );
        notificationAccessController =
                EnvHelp.getNotificationAccessController(env);
    }

    public Integer addNotificationListener(final ObjectName name,
        final NotificationFilter filter)
        throws InstanceNotFoundException, IOException {

        if (logger.traceOn()) {
            logger.trace("addNotificationListener",
                "Add a listener at " + name);
        }

        checkState();

        // Explicitly check MBeanPermission for addNotificationListener
        //
        checkMBeanPermission(name, "addNotificationListener");
        if (notificationAccessController != null) {
            notificationAccessController.addNotificationListener(
                connectionId, name, getSubject());
        }
        try {
            boolean instanceOf =
            AccessController.doPrivileged(
                    new PrivilegedExceptionAction<Boolean>() {
                        public Boolean run() throws InstanceNotFoundException {
                            return mbeanServer.isInstanceOf(name, broadcasterClass);
                        }
            });
            if (!instanceOf) {
                throw new IllegalArgumentException("The specified MBean [" +
                    name + "] is not a " +
                    "NotificationBroadcaster " +
                    "object.");
            }
        } catch (PrivilegedActionException e) {
            throw (InstanceNotFoundException) extractException(e);
        }

        final Integer id = getListenerID();

        // 6238731: set the default domain if no domain is set.
        ObjectName nn = name;
        if (name.getDomain() == null || name.getDomain().isEmpty()) {
            try {
                nn = ObjectName.getInstance(mbeanServer.getDefaultDomain(),
                                            name.getKeyPropertyList());
            } catch (MalformedObjectNameException mfoe) {
                // impossible, but...
                IOException ioe = new IOException(mfoe.getMessage());
                ioe.initCause(mfoe);
                throw ioe;
            }
        }

        synchronized (listenerMap) {
            IdAndFilter idaf = new IdAndFilter(id, filter);
            Set<IdAndFilter> set = listenerMap.get(nn);
            // Tread carefully because if set.size() == 1 it may be the
            // Collections.singleton we make here, which is unmodifiable.
            if (set == null)
                set = Collections.singleton(idaf);
            else {
                if (set.size() == 1)
                    set = new HashSet<IdAndFilter>(set);
                set.add(idaf);
            }
            listenerMap.put(nn, set);
        }

        return id;
    }

    public void removeNotificationListener(ObjectName name,
        Integer[] listenerIDs)
        throws Exception {

        if (logger.traceOn()) {
            logger.trace("removeNotificationListener",
                "Remove some listeners from " + name);
        }

        checkState();

        // Explicitly check MBeanPermission for removeNotificationListener
        //
        checkMBeanPermission(name, "removeNotificationListener");
        if (notificationAccessController != null) {
            notificationAccessController.removeNotificationListener(
                connectionId, name, getSubject());
        }

        Exception re = null;
        for (int i = 0 ; i < listenerIDs.length ; i++) {
            try {
                removeNotificationListener(name, listenerIDs[i]);
            } catch (Exception e) {
                // Give back the first exception
                //
                if (re != null) {
                    re = e;
                }
            }
        }
        if (re != null) {
            throw re;
        }
    }

    public void removeNotificationListener(ObjectName name, Integer listenerID)
    throws
        InstanceNotFoundException,
        ListenerNotFoundException,
        IOException {

        if (logger.traceOn()) {
            logger.trace("removeNotificationListener",
                "Remove the listener " + listenerID + " from " + name);
        }

        checkState();

        if (name != null && !name.isPattern()) {
            if (!mbeanServer.isRegistered(name)) {
                throw new InstanceNotFoundException("The MBean " + name +
                    " is not registered.");
            }
        }

        synchronized (listenerMap) {
            // Tread carefully because if set.size() == 1 it may be a
            // Collections.singleton, which is unmodifiable.
            Set<IdAndFilter> set = listenerMap.get(name);
            IdAndFilter idaf = new IdAndFilter(listenerID, null);
            if (set == null || !set.contains(idaf))
                throw new ListenerNotFoundException("Listener not found");
            if (set.size() == 1)
                listenerMap.remove(name);
            else
                set.remove(idaf);
        }
    }

    /* This is the object that will apply our filtering to candidate
     * notifications.  First of all, if there are no listeners for the
     * ObjectName that the notification is coming from, we go no further.
     * Then, for each listener, we must apply the corresponding filter (if any)
     * and ignore the listener if the filter rejects.  Finally, we apply
     * some access checks which may also reject the listener.
     *
     * A given notification may trigger several listeners on the same MBean,
     * which is why listenerMap is a Map<ObjectName, Set<IdAndFilter>> and
     * why we add the found notifications to a supplied List rather than
     * just returning a boolean.
     */
    private final NotifForwarderBufferFilter bufferFilter = new NotifForwarderBufferFilter();

    final class NotifForwarderBufferFilter implements NotificationBufferFilter {
        public void apply(List<TargetedNotification> targetedNotifs,
                          ObjectName source, Notification notif) {
            // We proceed in two stages here, to avoid holding the listenerMap
            // lock while invoking the filters (which are user code).
            final IdAndFilter[] candidates;
            synchronized (listenerMap) {
                final Set<IdAndFilter> set = listenerMap.get(source);
                if (set == null) {
                    logger.debug("bufferFilter", "no listeners for this name");
                    return;
                }
                candidates = new IdAndFilter[set.size()];
                set.toArray(candidates);
            }
            // We don't synchronize on targetedNotifs, because it is a local
            // variable of our caller and no other thread can see it.
            for (IdAndFilter idaf : candidates) {
                final NotificationFilter nf = idaf.getFilter();
                if (nf == null || nf.isNotificationEnabled(notif)) {
                    logger.debug("bufferFilter", "filter matches");
                    final TargetedNotification tn =
                            new TargetedNotification(notif, idaf.getId());
                    if (allowNotificationEmission(source, tn))
                        targetedNotifs.add(tn);
                }
            }
        }
    };

    public NotificationResult fetchNotifs(long startSequenceNumber,
        long timeout,
        int maxNotifications) {
        if (logger.traceOn()) {
            logger.trace("fetchNotifs", "Fetching notifications, the " +
                "startSequenceNumber is " + startSequenceNumber +
                ", the timeout is " + timeout +
                ", the maxNotifications is " + maxNotifications);
        }

        NotificationResult nr;
        final long t = Math.min(connectionTimeout, timeout);
        try {
            nr = notifBuffer.fetchNotifications(bufferFilter,
                startSequenceNumber,
                t, maxNotifications);
            snoopOnUnregister(nr);
        } catch (InterruptedException ire) {
            nr = new NotificationResult(0L, 0L, new TargetedNotification[0]);
        }

        if (logger.traceOn()) {
            logger.trace("fetchNotifs", "Forwarding the notifs: "+nr);
        }

        return nr;
    }

    // The standard RMI connector client will register a listener on the MBeanServerDelegate
    // in order to be told when MBeans are unregistered.  We snoop on fetched notifications
    // so that we can know too, and remove the corresponding entry from the listenerMap.
    // See 6957378.
    private void snoopOnUnregister(NotificationResult nr) {
        List<IdAndFilter> copy = null;
        synchronized (listenerMap) {
            Set<IdAndFilter> delegateSet = listenerMap.get(MBeanServerDelegate.DELEGATE_NAME);
            if (delegateSet == null || delegateSet.isEmpty()) {
                return;
            }
            copy = new ArrayList<>(delegateSet);
        }

        for (TargetedNotification tn : nr.getTargetedNotifications()) {
            Integer id = tn.getListenerID();
            for (IdAndFilter idaf : copy) {
                if (idaf.id == id) {
                    // This is a notification from the MBeanServerDelegate.
                    Notification n = tn.getNotification();
                    if (n instanceof MBeanServerNotification &&
                            n.getType().equals(MBeanServerNotification.UNREGISTRATION_NOTIFICATION)) {
                        MBeanServerNotification mbsn = (MBeanServerNotification) n;
                        ObjectName gone = mbsn.getMBeanName();
                        synchronized (listenerMap) {
                            listenerMap.remove(gone);
                        }
                    }
                }
            }
        }
    }

    public void terminate() {
        if (logger.traceOn()) {
            logger.trace("terminate", "Be called.");
        }

        synchronized(terminationLock) {
            if (terminated) {
                return;
            }

            terminated = true;

            synchronized(listenerMap) {
                listenerMap.clear();
            }
        }

        if (logger.traceOn()) {
            logger.trace("terminate", "Terminated.");
        }
    }

    //----------------
    // PRIVATE METHODS
    //----------------

    private Subject getSubject() {
        return Subject.getSubject(AccessController.getContext());
    }

    private void checkState() throws IOException {
        synchronized(terminationLock) {
            if (terminated) {
                throw new IOException("The connection has been terminated.");
            }
        }
    }

    private Integer getListenerID() {
        synchronized(listenerCounterLock) {
            return listenerCounter++;
        }
    }

    
Explicitly check the MBeanPermission for the current access control context.
/** * Explicitly check the MBeanPermission for * the current access control context. */
public final void checkMBeanPermission( final ObjectName name, final String actions) throws InstanceNotFoundException, SecurityException { checkMBeanPermission(mbeanServer,name,actions); } static void checkMBeanPermission( final MBeanServer mbs, final ObjectName name, final String actions) throws InstanceNotFoundException, SecurityException { SecurityManager sm = System.getSecurityManager(); if (sm != null) { AccessControlContext acc = AccessController.getContext(); ObjectInstance oi; try { oi = AccessController.doPrivileged( new PrivilegedExceptionAction<ObjectInstance>() { public ObjectInstance run() throws InstanceNotFoundException { return mbs.getObjectInstance(name); } }); } catch (PrivilegedActionException e) { throw (InstanceNotFoundException) extractException(e); } String classname = oi.getClassName(); MBeanPermission perm = new MBeanPermission( classname, null, name, actions); sm.checkPermission(perm, acc); } }
Check if the caller has the right to get the following notifications.
/** * Check if the caller has the right to get the following notifications. */
private boolean allowNotificationEmission(ObjectName name, TargetedNotification tn) { try { if (checkNotificationEmission) { checkMBeanPermission(name, "addNotificationListener"); } if (notificationAccessController != null) { notificationAccessController.fetchNotification( connectionId, name, tn.getNotification(), getSubject()); } return true; } catch (SecurityException e) { if (logger.debugOn()) { logger.debug("fetchNotifs", "Notification " + tn.getNotification() + " not forwarded: the " + "caller didn't have the required access rights"); } return false; } catch (Exception e) { if (logger.debugOn()) { logger.debug("fetchNotifs", "Notification " + tn.getNotification() + " not forwarded: " + "got an unexpected exception: " + e); } return false; } }
Iterate until we extract the real exception from a stack of PrivilegedActionExceptions.
/** * Iterate until we extract the real exception * from a stack of PrivilegedActionExceptions. */
private static Exception extractException(Exception e) { while (e instanceof PrivilegedActionException) { e = ((PrivilegedActionException)e).getException(); } return e; } private static class IdAndFilter { private Integer id; private NotificationFilter filter; IdAndFilter(Integer id, NotificationFilter filter) { this.id = id; this.filter = filter; } Integer getId() { return this.id; } NotificationFilter getFilter() { return this.filter; } @Override public int hashCode() { return id.hashCode(); } @Override public boolean equals(Object o) { return ((o instanceof IdAndFilter) && ((IdAndFilter) o).getId().equals(getId())); } } //------------------ // PRIVATE VARIABLES //------------------ private MBeanServer mbeanServer; private final String connectionId; private final long connectionTimeout; private static int listenerCounter = 0; private final static int[] listenerCounterLock = new int[0]; private NotificationBuffer notifBuffer; private final Map<ObjectName, Set<IdAndFilter>> listenerMap = new HashMap<ObjectName, Set<IdAndFilter>>(); private boolean terminated = false; private final int[] terminationLock = new int[0]; static final String broadcasterClass = NotificationBroadcaster.class.getName(); private final boolean checkNotificationEmission; private final NotificationAccessController notificationAccessController; private static final ClassLogger logger = new ClassLogger("javax.management.remote.misc", "ServerNotifForwarder"); }