/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.component.jmx;

import java.util.Hashtable;
import javax.management.MalformedObjectNameException;
import javax.management.NotificationFilter;
import javax.management.ObjectName;

import org.apache.camel.Consumer;
import org.apache.camel.Processor;
import org.apache.camel.Producer;
import org.apache.camel.impl.DefaultEndpoint;
import org.apache.camel.util.ObjectHelper;

/**
 * Endpoint that describes a connection to an mbean.
 * <p/>
 * The component can connect to the local platform mbean server with the following URI:
 * <p/>
 * <code>jmx://platform?options</code>
 * <p/>
 * A remote mbean server url can be provided following the initial JMX scheme like so:
 * <p/>
 * <code>jmx:service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi?options</code>
 * <p/>
 * You can append query options to the URI in the following format, ?options=value&option2=value&...
 */
public class JMXEndpoint extends DefaultEndpoint {

    // error messages as constants so they can be asserted on from unit tests
    protected static final String ERR_PLATFORM_SERVER = "Monitor type consumer only supported on platform server.";
    protected static final String ERR_THRESHOLD_LOW = "ThresholdLow must be set when monitoring a gauge attribute.";
    protected static final String ERR_THRESHOLD_HIGH = "ThresholdHigh must be set when monitoring a gauge attribute.";
    protected static final String ERR_GAUGE_NOTIFY = "One or both of NotifyHigh and NotifyLow must be true when monitoring a gauge attribute.";
    protected static final String ERR_STRING_NOTIFY = "One or both of NotifyDiffer and NotifyMatch must be true when monitoring a string attribute.";
    protected static final String ERR_STRING_TO_COMPARE = "StringToCompare must be specified when monitoring a string attribute.";
    protected static final String ERR_OBSERVED_ATTRIBUTE = "Observed attribute must be specified";

    /**
     * URI Property: [monitor types only] The attribute to observe for the monitor bean.  
     */
    private String mObservedAttribute;

    /**
     * URI Property: [monitor types only] The frequency to poll the bean to check the monitor.  
     */
    private long mGranularityPeriod;

    /**
     * URI Property: [monitor types only] The type of monitor to create. One of string, gauge, counter.  
     */
    private String mMonitorType;

    /**
     * URI Property: [counter monitor only] Initial threshold for the monitor. The value must exceed this before notifications are fired.  
     */
    private int mInitThreshold;

    /**
     * URI Property: [counter monitor only] The amount to increment the threshold after it's been exceeded.  
     */
    private int mOffset;

    /**
     * URI Property: [counter monitor only] The value at which the counter is reset to zero  
     */
    private int mModulus;

    /**
     * URI Property: [counter + gauge monitor only] If true, then the value reported in the notification is the difference from the threshold as opposed to the value itself.  
     */
    private boolean mDifferenceMode;

    /**
     * URI Property: [gauge monitor only] If true, the gauge will fire a notification when the high threshold is exceeded  
     */
    private boolean mNotifyHigh;

    /**
     * URI Property: [gauge monitor only] If true, the gauge will fire a notification when the low threshold is exceeded  
     */
    private boolean mNotifyLow;

    /**
     * URI Property: [gauge monitor only] Value for the gauge's high threshold  
     */
    private Double mThresholdHigh;

    /**
     * URI Property: [gauge monitor only] Value for the gauge's low threshold  
     */
    private Double mThresholdLow;

    /**
     * URI Property: [string monitor only] If true, the string monitor will fire a notification when the string attribute differs from the string to compare.  
     */
    private boolean mNotifyDiffer;

    /**
     * URI Property: [string monitor only] If true, the string monitor will fire a notification when the string attribute matches the string to compare.  
     */
    private boolean mNotifyMatch;

    /**
     * URI Property: [string monitor only] Value for the string monitor's string to compare.  
     */
    private String mStringToCompare;
    
    /**
     * URI Property: Format for the message body. Either "xml" or "raw". If xml, the notification is serialized to xml. If raw, then the raw java object is set as the body.
     */
    private String mFormat = "xml";

    /**
     * URI Property: credentials for making a remote connection
     */
    private String mUser;

    /**
     * URI Property: credentials for making a remote connection
     */
    private String mPassword;

    /**
     * URI Property: The domain for the mbean you're connecting to
     */
    private String mObjectDomain;

    /**
     * URI Property: The name key for the mbean you're connecting to. This value is mutually exclusive with the object properties that get passed.
     */
    private String mObjectName;

    /**
     * URI Property: Reference to a bean that implements the NotificationFilter.
     */
    private NotificationFilter mNotificationFilter;

    /**
     * URI Property: Value to handback to the listener when a notification is received. This value will be put in the message header with the key "jmx.handback"
     */
    private Object mHandback;
    
    /**
     * URI Property:  If true the consumer will throw an exception if unable to establish the JMX connection upon startup.  If false, the consumer will attempt
     *                to establish the JMX connection every 'x' seconds until the connection is made -- where 'x' is the configured  reconnectionDelay 
     */
    private boolean mTestConnectionOnStartup = true;
    
    
    /**
     * URI Property:  If true the consumer will attempt to reconnect to the JMX server when any connection failure occurs.  The consumer will attempt
     *                to re-establish the JMX connection every 'x' seconds until the connection is made-- where 'x' is the configured  reconnectionDelay
     */
    private boolean mReconnectOnConnectionFailure;
     
     /**
      * URI Property:  The number of seconds to wait before attempting to retry establishment of the initial connection or attempt to reconnect a lost connection
      */
    private int mReconnectDelay = 10;

    /**
     * URI Property: properties for the object name. These values will be used if the objectName param is not set
     */
    private Hashtable<String, String> mObjectProperties;

    /**
     * cached object name that was built from the objectName param or the hashtable
     */
    private ObjectName mJMXObjectName;
    /**
     * server url comes from the remaining endpoint
     */
    private String mServerURL;

    public JMXEndpoint(String aEndpointUri, JMXComponent aComponent) {
        super(aEndpointUri, aComponent);
    }

    public Consumer createConsumer(Processor aProcessor) throws Exception {
        // validate that all of the endpoint is configured properly
        if (getMonitorType() != null) {
            
            if (!isPlatformServer()) {
                throw new IllegalArgumentException(ERR_PLATFORM_SERVER);
            }
            
            if (ObjectHelper.isEmpty(getObservedAttribute())) {
                throw new IllegalArgumentException(ERR_OBSERVED_ATTRIBUTE);
            }
            if (getMonitorType().equals("string")) {
                if (ObjectHelper.isEmpty(getStringToCompare())) {
                    throw new IllegalArgumentException(ERR_STRING_TO_COMPARE);
                }
                if (!isNotifyDiffer() && !isNotifyMatch()) {
                    throw new IllegalArgumentException(ERR_STRING_NOTIFY);
                }
            } else if (getMonitorType().equals("gauge")) {
                if (!isNotifyHigh() && !isNotifyLow()) {
                    throw new IllegalArgumentException(ERR_GAUGE_NOTIFY);
                }
                if (getThresholdHigh() == null) {
                    throw new IllegalArgumentException(ERR_THRESHOLD_HIGH);
                }
                if (getThresholdLow() == null) {
                    throw new IllegalArgumentException(ERR_THRESHOLD_LOW);
                }
            }
            JMXMonitorConsumer answer = new JMXMonitorConsumer(this, aProcessor);
            configureConsumer(answer);
            return answer;
        } else {
            // shouldn't need any other validation.
            JMXConsumer answer = new JMXConsumer(this, aProcessor);
            configureConsumer(answer);
            return answer;
        }
    }

    public Producer createProducer() throws Exception {
        throw new UnsupportedOperationException("producing JMX notifications is not supported");
    }

    public boolean isSingleton() {
        return false;
    }

    public String getFormat() {
        return mFormat;
    }

    public void setFormat(String aFormat) {
        mFormat = aFormat;
    }

    public boolean isXML() {
        return "xml".equals(getFormat());
    }

    public boolean isPlatformServer() {
        return "platform".equals(getServerURL());
    }

    public String getUser() {
        return mUser;
    }

    public void setUser(String aUser) {
        mUser = aUser;
    }

    public String getPassword() {
        return mPassword;
    }

    public void setPassword(String aPassword) {
        mPassword = aPassword;
    }

    public String getObjectDomain() {
        return mObjectDomain;
    }

    public void setObjectDomain(String aObjectDomain) {
        mObjectDomain = aObjectDomain;
    }

    public String getObjectName() {
        return mObjectName;
    }

    public void setObjectName(String aObjectName) {
        if (getObjectProperties() != null) {
            throw new IllegalArgumentException("Cannot set both objectName and objectProperties");
        }
        mObjectName = aObjectName;
    }

    protected String getServerURL() {
        return mServerURL;
    }

    protected void setServerURL(String aServerURL) {
        mServerURL = aServerURL;
    }

    public NotificationFilter getNotificationFilter() {
        return mNotificationFilter;
    }

    public void setNotificationFilter(NotificationFilter aFilterRef) {
        mNotificationFilter = aFilterRef;
    }

    public Object getHandback() {
        return mHandback;
    }

    public void setHandback(Object aHandback) {
        mHandback = aHandback;
    }

    public Hashtable<String, String> getObjectProperties() {
        return mObjectProperties;
    }

    /**
     * Setter for the ObjectProperties is either called by reflection when
     * processing the URI or manually by the component.
     * <p/>
     * If the URI contained a value with a reference like "objectProperties=#myHashtable"
     * then the Hashtable will be set in place.
     * <p/>
     * If there are extra properties that begin with "key." then the component will
     * create a Hashtable with these values after removing the "key." prefix.
     */
    public void setObjectProperties(Hashtable<String, String> aObjectProperties) {
        if (getObjectName() != null) {
            throw new IllegalArgumentException("Cannot set both objectName and objectProperties");
        }
        mObjectProperties = aObjectProperties;
    }

    protected ObjectName getJMXObjectName() throws MalformedObjectNameException {
        if (mJMXObjectName == null) {
            ObjectName on = buildObjectName();
            setJMXObjectName(on);
        }
        return mJMXObjectName;
    }

    protected void setJMXObjectName(ObjectName aCachedObjectName) {
        mJMXObjectName = aCachedObjectName;
    }

    public String getObservedAttribute() {
        return mObservedAttribute;
    }

    public void setObservedAttribute(String aObservedAttribute) {
        mObservedAttribute = aObservedAttribute;
    }

    public long getGranularityPeriod() {
        return mGranularityPeriod;
    }

    public void setGranularityPeriod(long aGranularityPeriod) {
        mGranularityPeriod = aGranularityPeriod;
    }

    public String getMonitorType() {
        return mMonitorType;
    }

    public void setMonitorType(String aMonitorType) {
        mMonitorType = aMonitorType;
    }

    public int getInitThreshold() {
        return mInitThreshold;
    }

    public void setInitThreshold(int aInitThreshold) {
        mInitThreshold = aInitThreshold;
    }

    public int getOffset() {
        return mOffset;
    }

    public void setOffset(int aOffset) {
        mOffset = aOffset;
    }

    public int getModulus() {
        return mModulus;
    }

    public void setModulus(int aModulus) {
        mModulus = aModulus;
    }

    public boolean isDifferenceMode() {
        return mDifferenceMode;
    }

    public void setDifferenceMode(boolean aDifferenceMode) {
        mDifferenceMode = aDifferenceMode;
    }

    public boolean isNotifyHigh() {
        return mNotifyHigh;
    }

    public void setNotifyHigh(boolean aNotifyHigh) {
        mNotifyHigh = aNotifyHigh;
    }

    public boolean isNotifyLow() {
        return mNotifyLow;
    }

    public void setNotifyLow(boolean aNotifyLow) {
        mNotifyLow = aNotifyLow;
    }

    public Double getThresholdHigh() {
        return mThresholdHigh;
    }

    public void setThresholdHigh(Double aThresholdHigh) {
        mThresholdHigh = aThresholdHigh;
    }

    public Double getThresholdLow() {
        return mThresholdLow;
    }

    public void setThresholdLow(Double aThresholdLow) {
        mThresholdLow = aThresholdLow;
    }

    public boolean isNotifyDiffer() {
        return mNotifyDiffer;
    }

    public void setNotifyDiffer(boolean aNotifyDiffer) {
        mNotifyDiffer = aNotifyDiffer;
    }

    public boolean isNotifyMatch() {
        return mNotifyMatch;
    }

    public void setNotifyMatch(boolean aNotifyMatch) {
        mNotifyMatch = aNotifyMatch;
    }

    public String getStringToCompare() {
        return mStringToCompare;
    }

    public void setStringToCompare(String aStringToCompare) {
        mStringToCompare = aStringToCompare;
    }
    
    public boolean getTestConnectionOnStartup() {
        return this.mTestConnectionOnStartup;
    }
    
    public void setTestConnectionOnStartup(boolean testConnectionOnStartup) {
        this.mTestConnectionOnStartup = testConnectionOnStartup;
    }
    
    public boolean getReconnectOnConnectionFailure() {
        return this.mReconnectOnConnectionFailure;
    }
    
    public void setReconnectOnConnectionFailure(boolean reconnectOnConnectionFailure) {
        this.mReconnectOnConnectionFailure = reconnectOnConnectionFailure;
    }    
    
    public int getReconnectDelay() {
        return this.mReconnectDelay;
    }    
     
    public void setReconnectDelay(int reconnectDelay) {
        this.mReconnectDelay = reconnectDelay;
    }
     
    private ObjectName buildObjectName() throws MalformedObjectNameException {
        ObjectName objectName;
        if (getObjectProperties() == null) {
            StringBuilder sb = new StringBuilder(getObjectDomain()).append(':').append("name=").append(getObjectName());
            objectName = new ObjectName(sb.toString());
        } else {
            objectName = new ObjectName(getObjectDomain(), getObjectProperties());
        }
        return objectName;
    }
}
