001/*
002 * #%L
003 * Netarchivesuite - monitor
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023
024package dk.netarkivet.monitor.jmx;
025
026import java.io.IOException;
027import java.util.Set;
028import java.util.concurrent.atomic.AtomicBoolean;
029
030import javax.management.MBeanServerConnection;
031import javax.management.MBeanServerInvocationHandler;
032import javax.management.MalformedObjectNameException;
033import javax.management.ObjectName;
034
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038import dk.netarkivet.common.exceptions.ArgumentNotValid;
039import dk.netarkivet.common.exceptions.IOFailure;
040import dk.netarkivet.common.utils.JMXUtils;
041import dk.netarkivet.common.utils.Settings;
042import dk.netarkivet.monitor.MonitorSettings;
043
044/** Creates RMI-based JMX connections to remote servers. */
045public class RmiProxyConnectionFactory implements JMXProxyConnectionFactory {
046
047    /**
048     * Returns a JMXProxyFactory for a specific server, jmxport, rmiport, username, and password. Makes sure that an
049     * initial context for JNDI has been specified. Then constructs a RMI-based JMXServiceUrl using the server and port.
050     * Finally connects to the URL using the name and password.
051     *
052     * @param server the given remote server
053     * @param jmxPort the JMX port on that server
054     * @param rmiPort the RMI port on that server (dedicated to the above jmxPort)
055     * @param userName the userName for access to the MBeanserver on that server
056     * @param password the password for access to the MBeanserver on that server
057     * @return a JMXProxyFactory with the above properties.
058     */
059    public JMXProxyConnection getConnection(String server, int jmxPort, int rmiPort, String userName, String password) {
060        ArgumentNotValid.checkNotNullOrEmpty(server, "String server");
061        ArgumentNotValid.checkNotNegative(jmxPort, "int jmxPort");
062        ArgumentNotValid.checkNotNegative(rmiPort, "int rmiPort");
063        ArgumentNotValid.checkNotNullOrEmpty(userName, "String userName");
064        ArgumentNotValid.checkNotNullOrEmpty(password, "String password");
065        return new MBeanServerProxyConnection(server, jmxPort, rmiPort, userName, password);
066    }
067
068    /**
069     * A JMXProxyFactory that constructs proxies by forwarding method calls through an MBeanServerConnection.
070     */
071    private static class MBeanServerProxyConnection implements JMXProxyConnection {
072        /** The connection to use for method call forwarding. */
073        private MBeanServerConnection connection;
074        /** Whether we are currently in the process of connecting. */
075        private final AtomicBoolean connecting = new AtomicBoolean(false);
076        /** The given remote server. */
077        private String server;
078        /** The JMX port on the server. */
079        private int jmxPort;
080        /** The RMI call back port on the server. */
081        private int rmiPort;
082        /** The JMX username on the server. */
083        private String userName;
084        /** The JMX password on the server. */
085        private String password;
086        /** The class logger. */
087        private static final Logger log = LoggerFactory.getLogger(MBeanServerProxyConnection.class);
088        /** How long to wait for the proxied JMX connection in milliseconds. */
089        private static final long JMX_TIMEOUT = Settings.getLong(MonitorSettings.JMX_PROXY_TIMEOUT);
090
091        /**
092         * Proxies an MBean connection with the given parameters.
093         *
094         * @param server the given remote server
095         * @param jmxPort the JMX port on that server
096         * @param rmiPort the RMI port on that server (dedicated to the above jmxPort)
097         * @param userName the userName for access to the MBeanserver on that server
098         * @param password the password for access to the MBeanserver on that server
099         */
100        public MBeanServerProxyConnection(final String server, final int jmxPort, final int rmiPort,
101                final String userName, final String password) {
102            this.server = server;
103            this.jmxPort = jmxPort;
104            this.rmiPort = rmiPort;
105            this.userName = userName;
106            this.password = password;
107            connect();
108        }
109
110        /**
111         * Initialise a thread to connect to the remote server. This method does not wait for the connection to finish,
112         * so there is no guarantee that the connection is initialised at the end of this method. Ensures that we only
113         * do one connect() operation at a time.
114         */
115        private void connect() {
116            new Thread() {
117                public void run() {
118                    if (connection == null && connecting.compareAndSet(false, true)) {
119                        log.debug("Trying to connect to remote JMX server '{}', port '{}', rmiPort '{}', user '{}'", server,
120                                jmxPort, rmiPort, userName);
121                        try {
122                            connection = JMXUtils
123                                    .getMBeanServerConnection(server, jmxPort, rmiPort, userName, password);
124                            log.info("Connected to remote JMX server '{}', port '{}', rmiPort '{}', user '{}'", server,
125                                    jmxPort, rmiPort, userName);
126                        } catch (Exception e) {
127                            log.warn("Unable to connect to remote JMX server '{}', port '{}', rmiPort '{}', user '{}'",
128                                    server, jmxPort, rmiPort, userName, e);
129                        } finally {
130                            connecting.set(false);
131                            synchronized (connecting) {
132                                connecting.notifyAll();
133                            }
134                        }
135                    }
136                }
137            }.start();
138        }
139
140        /**
141         * Sleep until the timeout has occurred, or connection is successful.
142         */
143        private void waitForConnection() {
144            long timeouttime = System.currentTimeMillis() + JMX_TIMEOUT;
145            while (connecting.get() && (timeouttime - System.currentTimeMillis() > 0)) {
146                try {
147                    synchronized (connecting) {
148                        connecting.wait(Math.max(timeouttime - System.currentTimeMillis(), 1));
149                    }
150                } catch (InterruptedException e) {
151                    // Just ignore it
152                }
153            }
154        }
155
156        /**
157         * Return object names from remote location.
158         *
159         * @param query The query remote mbeans should match
160         * @return set of names of matching mbeans.
161         * @throws IOFailure on communication trouble.
162         * @throws ArgumentNotValid on null or empty query.
163         */
164        public Set<ObjectName> query(String query) {
165            ArgumentNotValid.checkNotNullOrEmpty(query, "String query");
166            if (connection == null) {
167                connect();
168                waitForConnection();
169            }
170            if (connection == null) {
171                throw new IOFailure("Could not get connection for query '" + query + "'");
172            }
173            log.debug("Trying to retrieve MBeanNames from remote JMX server '{}', port '{}', rmiPort '{}', user '{}' matching the query {}",
174                                    server, jmxPort, rmiPort, userName, query);
175            try {
176                return connection.queryNames(new ObjectName(query), null);
177            } catch (IOException e) {
178                throw new IOFailure("Unable to query for remote mbeans matching '" + query + "'", e);
179            } catch (MalformedObjectNameException e) {
180                throw new IOFailure("Couldn't construct the objectName with string argument:' " + query + "'.", e);
181            }
182        }
183
184        /**
185         * Returns true if this object still can return usable proxies.
186         *
187         * @return True if we can return usable proxies. Otherwise, somebody may have to make a new instance of
188         * JMXProxyFactory to get new proxies.
189         */
190        public boolean isLive() {
191            if (connection == null) {
192                connect();
193                waitForConnection();
194                if (connection == null) {
195                    return false;
196                }
197            }
198            try {
199                this.connection.getMBeanCount();
200                return true;
201            } catch (Exception e) {
202                /*
203                 * Catching the exception appears to be the only way to check that the connection is dead.
204                 */
205                return false;
206            }
207        }
208
209        /**
210         * Uses Java's built-in facilities for creating proxies to remote MBeans. Does not support notifications.
211         *
212         * @param name The name of an MBean on the registered MBeanServerConnection
213         * @param intf The interface that the returned proxy should implement.
214         * @param <T> the type of class the argument intf is, and the return type.
215         * @return an object implementing T. This object forwards all method calls to the MBean registered under the
216         * given name on the MBeanServerConnection that we use.
217         */
218        public <T> T createProxy(ObjectName name, Class<T> intf) {
219            ArgumentNotValid.checkNotNull(name, "ObjectName name");
220            ArgumentNotValid.checkNotNull(intf, "Class<T> intf");
221            // Set true to enable notifications
222            return MBeanServerInvocationHandler.newProxyInstance(connection, name, intf, false);
223        }
224    }
225
226}