001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.logging.log4j.core.net;
018    
019    import java.lang.reflect.Constructor;
020    import java.lang.reflect.InvocationTargetException;
021    import java.lang.reflect.Method;
022    import java.util.HashMap;
023    import java.util.Hashtable;
024    import java.util.Map;
025    import org.apache.logging.log4j.Logger;
026    import org.apache.logging.log4j.core.config.plugins.Plugin;
027    import org.apache.logging.log4j.status.StatusLogger;
028    
029    /**
030     * Advertise an entity via ZeroConf/MulticastDNS and the JmDNS library.
031     *
032     * The length of property names and values must be 255 bytes or less.  
033     * Entries with names or values larger than 255 bytes will be removed prior to advertisement.
034     *
035     */
036    @Plugin(name = "multicastdns", category = "Core", elementType = "advertiser", printObject = false)
037    public class MulticastDNSAdvertiser implements Advertiser {
038        protected static final Logger LOGGER = StatusLogger.getLogger();
039        private static Object jmDNS = initializeJMDNS();
040    
041        private static Class<?> jmDNSClass;
042        private static Class<?> serviceInfoClass;
043    
044        public MulticastDNSAdvertiser()
045        {
046            //no arg constructor for reflection
047        }
048    
049        /**
050         * Advertise the provided entity.
051         *
052         * Properties map provided in advertise method must include a "name" entry
053         * but may also provide "protocol" (tcp/udp) as well as a "port" entry
054         *
055         * The length of property names and values must be 255 bytes or less.  
056         * Entries with names or values larger than 255 bytes will be removed prior to advertisement.
057         * 
058         * @param properties the properties representing the entity to advertise
059         * @return the object which can be used to unadvertise, or null if advertisement was unsuccessful
060         */
061        @Override
062        public Object advertise(Map<String, String> properties) {
063            //default to tcp if "protocol" was not set
064            Map<String, String> truncatedProperties = new HashMap<String, String>();
065            for (Map.Entry<String, String> entry:properties.entrySet())
066            {
067                if (entry.getKey().length() <= 255 && entry.getValue().length() <= 255)
068                {
069                    truncatedProperties.put(entry.getKey(), entry.getValue());
070                }
071            }
072            String protocol = truncatedProperties.get("protocol");
073            String zone = "._log4j._"+(protocol != null ? protocol : "tcp") + ".local.";
074            //default to 4555 if "port" was not set
075            String portString = truncatedProperties.get("port");
076            int port = (portString != null ? Integer.parseInt(portString) : 4555);
077    
078            String name = truncatedProperties.get("name");
079    
080            //if version 3 is available, use it to construct a serviceInfo instance, otherwise support the version1 API
081            if (jmDNS != null)
082            {
083                boolean isVersion3 = false;
084                try {
085                    //create method is in version 3, not version 1
086                    jmDNSClass.getMethod("create", (Class[])null);
087                    isVersion3 = true;
088                } catch (NoSuchMethodException e) {
089                    //no-op
090                }
091                Object serviceInfo;
092                if (isVersion3) {
093                    serviceInfo = buildServiceInfoVersion3(zone, port, name, truncatedProperties);
094                } else {
095                    serviceInfo = buildServiceInfoVersion1(zone, port, name, truncatedProperties);
096                }
097    
098                try {
099                    Method method = jmDNSClass.getMethod("registerService", new Class[]{serviceInfoClass});
100                    method.invoke(jmDNS, serviceInfo);
101                } catch(IllegalAccessException e) {
102                    LOGGER.warn("Unable to invoke registerService method", e);
103                } catch(NoSuchMethodException e) {
104                    LOGGER.warn("No registerService method", e);
105                } catch(InvocationTargetException e) {
106                    LOGGER.warn("Unable to invoke registerService method", e);
107                }
108                return serviceInfo;
109            }
110            else
111            {
112                LOGGER.warn("JMDNS not available - will not advertise ZeroConf support");
113                return null;
114            }
115        }
116    
117        /**
118         * Unadvertise the previously advertised entity
119         * @param serviceInfo
120         */
121        @Override
122        public void unadvertise(Object serviceInfo) {
123            if (jmDNS != null) {
124                try {
125                    Method method = jmDNSClass.getMethod("unregisterService", new Class[]{serviceInfoClass});
126                    method.invoke(jmDNS, serviceInfo);
127                } catch(IllegalAccessException e) {
128                    LOGGER.warn("Unable to invoke unregisterService method", e);
129                } catch(NoSuchMethodException e) {
130                    LOGGER.warn("No unregisterService method", e);
131                } catch(InvocationTargetException e) {
132                    LOGGER.warn("Unable to invoke unregisterService method", e);
133                }
134            }
135        }
136    
137        private static Object createJmDNSVersion1()
138        {
139            try {
140                return jmDNSClass.newInstance();
141            } catch (InstantiationException e) {
142                LOGGER.warn("Unable to instantiate JMDNS", e);
143            } catch (IllegalAccessException e) {
144                LOGGER.warn("Unable to instantiate JMDNS", e);
145            }
146            return null;
147        }
148    
149        private static Object createJmDNSVersion3()
150        {
151            try {
152                Method jmDNSCreateMethod = jmDNSClass.getMethod("create", (Class[])null);
153                return jmDNSCreateMethod.invoke(null, (Object[])null);
154            } catch (IllegalAccessException e) {
155                LOGGER.warn("Unable to instantiate jmdns class", e);
156            } catch (NoSuchMethodException e) {
157                LOGGER.warn("Unable to access constructor", e);
158            } catch (InvocationTargetException e) {
159                LOGGER.warn("Unable to call constructor", e);
160            }
161            return null;
162        }
163    
164        private Object buildServiceInfoVersion1(String zone, int port, String name, Map<String, String> properties) {
165            //version 1 uses a hashtable
166            Hashtable<String, String> hashtableProperties = new Hashtable<String, String>(properties);
167            try {
168                Class[] args = new Class[6];
169                args[0] = String.class;
170                args[1] = String.class;
171                args[2] = int.class;
172                args[3] = int.class; //weight (0)
173                args[4] = int.class; //priority (0)
174                args[5] = Hashtable.class;
175                Constructor<?> constructor  = serviceInfoClass.getConstructor(args);
176                Object[] values = new Object[6];
177                values[0] = zone;
178                values[1] = name;
179                values[2] = port;
180                values[3] = 0;
181                values[4] = 0;
182                values[5] = hashtableProperties;
183                return constructor.newInstance(values);
184            } catch (IllegalAccessException e) {
185                LOGGER.warn("Unable to construct ServiceInfo instance", e);
186            } catch (NoSuchMethodException e) {
187                LOGGER.warn("Unable to get ServiceInfo constructor", e);
188            } catch (InstantiationException e) {
189                LOGGER.warn("Unable to construct ServiceInfo instance", e);
190            } catch (InvocationTargetException e) {
191                LOGGER.warn("Unable to construct ServiceInfo instance", e);
192            }
193            return null;
194        }
195    
196        private Object buildServiceInfoVersion3(String zone, int port, String name, Map<String, String> properties) {
197            try {
198                Class[] args = new Class[6];
199                args[0] = String.class; //zone/type
200                args[1] = String.class; //display name
201                args[2] = int.class; //port
202                args[3] = int.class; //weight (0)
203                args[4] = int.class; //priority (0)
204                args[5] = Map.class;
205                Method serviceInfoCreateMethod = serviceInfoClass.getMethod("create", args);
206                Object[] values = new Object[6];
207                values[0] = zone;
208                values[1] = name;
209                values[2] = port;
210                values[3] = 0;
211                values[4] = 0;
212                values[5] = properties;
213                return serviceInfoCreateMethod.invoke(null, values);
214            } catch (IllegalAccessException e) {
215                LOGGER.warn("Unable to invoke create method", e);
216            } catch (NoSuchMethodException e) {
217                LOGGER.warn("Unable to find create method", e);
218            } catch (InvocationTargetException e) {
219                LOGGER.warn("Unable to invoke create method", e);
220            }
221            return null;
222        }
223    
224        private static Object initializeJMDNS() {
225            try {
226                jmDNSClass = Class.forName("javax.jmdns.JmDNS");
227                serviceInfoClass = Class.forName("javax.jmdns.ServiceInfo");
228                //if version 3 is available, use it to constuct a serviceInfo instance, otherwise support the version1 API
229                boolean isVersion3 = false;
230                try {
231                    //create method is in version 3, not version 1
232                    jmDNSClass.getMethod("create", (Class[])null);
233                    isVersion3 = true;
234                } catch (NoSuchMethodException e) {
235                    //no-op
236                }
237    
238                if (isVersion3) {
239                    return createJmDNSVersion3();
240                } else {
241                    return createJmDNSVersion1();
242                }
243            } catch (ClassNotFoundException e) {
244                LOGGER.warn("JmDNS or serviceInfo class not found", e);
245            } catch (ExceptionInInitializerError e2) {
246                LOGGER.warn("JmDNS or serviceInfo class not found", e2);
247            }
248            return null;
249        }
250    }