001/**
002 *
003 * Copyright the original author or authors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * 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 */
017package org.jivesoftware.smackx.jingle.nat;
018
019import java.io.IOException;
020import java.net.ServerSocket;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.Iterator;
024import java.util.List;
025import java.util.logging.Logger;
026
027import org.jivesoftware.smack.SmackException;
028import org.jivesoftware.smack.SmackException.NotConnectedException;
029import org.jivesoftware.smack.XMPPException;
030import org.jivesoftware.smackx.jingle.JingleSession;
031
032/**
033 * A TransportResolver is used for obtaining a list of valid transport
034 * candidates. A transport candidate is composed by an IP address and a port number.
035 * It is called candidate, because it can be elected or not.
036 *
037 * @author Thiago Camargo
038 * @author Alvaro Saurin <alvaro.saurin@gmail.com>
039 */
040public abstract class TransportResolver {
041
042        private static final Logger LOGGER = Logger.getLogger(TransportResolver.class.getName());
043        
044        public enum Type {
045
046        rawupd, ice
047    }
048
049    ;
050
051    public Type getType() {
052        return type;
053    }
054
055    public void setType(Type type) {
056        this.type = type;
057    }
058
059    public Type type = Type.rawupd;
060
061    // the time, in milliseconds, before a check aborts
062    public static final int CHECK_TIMEOUT = 3000;
063
064    // Listeners for events
065    private final ArrayList<TransportResolverListener> listeners = new ArrayList<TransportResolverListener>();
066
067    // TRue if the resolver is working
068    private boolean resolving;
069
070    // This will be true when all the transport candidates have been gathered...
071    private boolean resolved;
072
073    // This indicates that a transport resolver is initialized
074    private boolean initialized = false;
075
076    // We store a list of candidates internally, just in case there are several
077    // possibilities. When the user asks for a transport, we return the best
078    // one.
079    protected final List<TransportCandidate> candidates = new ArrayList<TransportCandidate>();
080
081    /**
082     * Default constructor.
083     */
084    protected TransportResolver() {
085        super();
086
087        resolving = false;
088        resolved = false;
089    }
090
091    /**
092     * Initialize the Resolver
093     */
094    public abstract void initialize() throws XMPPException, SmackException;
095
096    /**
097     * Start a the resolution.
098     */
099    public abstract void resolve(JingleSession session) throws XMPPException, SmackException;
100
101    /**
102     * Clear the list of candidates and start a new resolution process.
103     *
104     * @throws XMPPException
105     */
106    public void clear() throws XMPPException {
107        cancel();
108        candidates.clear();
109    }
110
111    /**
112     * Cancel any asynchronous resolution operation.
113     */
114    public abstract void cancel() throws XMPPException;
115
116    /**
117     * Return true if the resolver is working.
118     *
119     * @return true if the resolver is working.
120     */
121    public boolean isResolving() {
122        return resolving;
123    }
124
125    /**
126     * Return true if the resolver has finished the search for transport
127     * candidates.
128     *
129     * @return true if the search has finished
130     */
131    public boolean isResolved() {
132        return resolved;
133    }
134
135    /**
136     * Set the Transport Resolver as initialized.
137     */
138    public synchronized void setInitialized() {
139        initialized = true;
140    }
141
142    /**
143     * Chack if the Transport Resolver is initialized
144     *
145     * @return true if initialized
146     */
147    public synchronized boolean isInitialized() {
148        return initialized;
149    }
150
151    /**
152     * Indicate the beggining of the resolution process. This method must be
153     * used by subclasses at the begining of their resolve() method.
154     */
155    protected synchronized void setResolveInit() {
156        resolved = false;
157        resolving = true;
158
159        triggerResolveInit();
160    }
161
162    /**
163     * Indicate the end of the resolution process. This method must be used by
164     * subclasses at the begining of their resolve() method.
165     */
166    protected synchronized void setResolveEnd() {
167        resolved = true;
168        resolving = false;
169
170        triggerResolveEnd();
171    }
172
173    // Listeners management
174
175    /**
176     * Add a transport resolver listener.
177     *
178     * @param li The transport resolver listener to be added.
179     */
180    public void addListener(TransportResolverListener li) {
181        synchronized (listeners) {
182            listeners.add(li);
183        }
184    }
185
186    /**
187     * Removes a transport resolver listener.
188     *
189     * @param li The transport resolver listener to be removed
190     */
191    public void removeListener(TransportResolverListener li) {
192        synchronized (listeners) {
193            listeners.remove(li);
194        }
195    }
196
197    /**
198     * Get the list of listeners
199     *
200     * @return the list of listeners
201     */
202    public ArrayList<TransportResolverListener> getListenersList() {
203        synchronized (listeners) {
204            return new ArrayList<TransportResolverListener>(listeners);
205        }
206    }
207
208    /**
209     * Trigger a new candidate added event.
210     *
211     * @param cand The candidate added to the list of candidates.
212     * @throws NotConnectedException 
213     */
214    protected void triggerCandidateAdded(TransportCandidate cand) throws NotConnectedException {
215        Iterator<TransportResolverListener> iter = getListenersList().iterator();
216        while (iter.hasNext()) {
217            TransportResolverListener trl = iter.next();
218            if (trl instanceof TransportResolverListener.Resolver) {
219                TransportResolverListener.Resolver li = (TransportResolverListener.Resolver) trl;
220                LOGGER.fine("triggerCandidateAdded : " + cand.getLocalIp());
221                li.candidateAdded(cand);
222            }
223        }
224    }
225
226    /**
227     * Trigger a event notifying the initialization of the resolution process.
228     */
229    private void triggerResolveInit() {
230        Iterator<TransportResolverListener> iter = getListenersList().iterator();
231        while (iter.hasNext()) {
232            TransportResolverListener trl = iter.next();
233            if (trl instanceof TransportResolverListener.Resolver) {
234                TransportResolverListener.Resolver li = (TransportResolverListener.Resolver) trl;
235                li.init();
236            }
237        }
238    }
239
240    /**
241     * Trigger a event notifying the obtention of all the candidates.
242     */
243    private void triggerResolveEnd() {
244        Iterator<TransportResolverListener> iter = getListenersList().iterator();
245        while (iter.hasNext()) {
246            TransportResolverListener trl = iter.next();
247            if (trl instanceof TransportResolverListener.Resolver) {
248                TransportResolverListener.Resolver li = (TransportResolverListener.Resolver) trl;
249                li.end();
250            }
251        }
252    }
253
254    // Candidates management
255
256    /**
257     * Clear the list of candidate
258     */
259    protected void clearCandidates() {
260        synchronized (candidates) {
261            candidates.clear();
262        }
263    }
264
265    /**
266     * Add a new transport candidate
267     *
268     * @param cand The candidate to add
269     * @throws NotConnectedException 
270     */
271    protected void addCandidate(TransportCandidate cand) throws NotConnectedException {
272        synchronized (candidates) {
273            if (!candidates.contains(cand))
274                candidates.add(cand);
275        }
276
277        // Notify the listeners
278        triggerCandidateAdded(cand);
279    }
280
281    /**
282     * Get an iterator for the list of candidates
283     *
284     * @return an iterator
285     */
286    public Iterator<TransportCandidate> getCandidates() {
287        synchronized (candidates) {
288            return Collections.unmodifiableList(new ArrayList<TransportCandidate>(candidates)).iterator();
289        }
290    }
291
292    /**
293     * Get the candididate with the highest preference.
294     *
295     * @return The best candidate, according to the preference order.
296     */
297    public TransportCandidate getPreferredCandidate() {
298        TransportCandidate result = null;
299
300        ArrayList<ICECandidate> cands = new ArrayList<ICECandidate>();
301        for (TransportCandidate tpcan : getCandidatesList()) {
302            if (tpcan instanceof ICECandidate)
303                cands.add((ICECandidate) tpcan);
304        }
305        
306        // (ArrayList<ICECandidate>) getCandidatesList();
307        if (cands.size() > 0) {
308            Collections.sort(cands);
309            // Return the last candidate
310            result = (TransportCandidate) cands.get(cands.size() - 1);
311            LOGGER.fine("Result: " + result.getIp());
312        }
313
314        return result;
315    }
316
317    /**
318     * Get the numer of transport candidates.
319     *
320     * @return The length of the transport candidates list.
321     */
322    public int getCandidateCount() {
323        synchronized (candidates) {
324            return candidates.size();
325        }
326    }
327
328    /**
329     * Get the list of candidates
330     *
331     * @return the list of transport candidates
332     */
333    public List<TransportCandidate> getCandidatesList() {
334        List<TransportCandidate> result = null;
335
336        synchronized (candidates) {
337            result = new ArrayList<TransportCandidate>(candidates);
338        }
339
340        return result;
341    }
342
343    /**
344     * Get the n-th candidate
345     *
346     * @return a transport candidate
347     */
348    public TransportCandidate getCandidate(int i) {
349        TransportCandidate cand;
350
351        synchronized (candidates) {
352            cand = (TransportCandidate) candidates.get(i);
353        }
354        return cand;
355    }
356
357    /**
358     * Initialize Transport Resolver and wait until it is complete unitialized.
359     * @throws SmackException 
360     */
361    public void initializeAndWait() throws XMPPException, SmackException {
362        this.initialize();
363        try {
364            LOGGER.fine("Initializing transport resolver...");
365            while (!this.isInitialized()) {
366                LOGGER.fine("Resolver init still pending");
367                Thread.sleep(1000);
368            }
369            LOGGER.fine("Transport resolved");
370        }
371        catch (Exception e) {
372            e.printStackTrace();
373        }
374    }
375
376    /**
377     * Obtain a free port we can use.
378     *
379     * @return A free port number.
380     */
381    protected int getFreePort() {
382        ServerSocket ss;
383        int freePort = 0;
384
385        for (int i = 0; i < 10; i++) {
386            freePort = (int) (10000 + Math.round(Math.random() * 10000));
387            freePort = freePort % 2 == 0 ? freePort : freePort + 1;
388            try {
389                ss = new ServerSocket(freePort);
390                freePort = ss.getLocalPort();
391                ss.close();
392                return freePort;
393            }
394            catch (IOException e) {
395                e.printStackTrace();
396            }
397        }
398        try {
399            ss = new ServerSocket(0);
400            freePort = ss.getLocalPort();
401            ss.close();
402        }
403        catch (IOException e) {
404            e.printStackTrace();
405        }
406        return freePort;
407    }
408}