View Javadoc

1   /*
2    * $Id: URIBuilder.java 22566 2011-07-27 22:52:53Z julien.eluard $
3    * --------------------------------------------------------------------------------------
4    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
5    *
6    * The software in this package is published under the terms of the CPAL v1.0
7    * license, a copy of which has been included with this distribution in the
8    * LICENSE.txt file.
9    */
10  
11  package org.mule.endpoint;
12  
13  import org.mule.api.AnnotatedObject;
14  import org.mule.api.MuleContext;
15  import org.mule.api.endpoint.EndpointException;
16  import org.mule.api.endpoint.EndpointURI;
17  import org.mule.util.ClassUtils;
18  
19  import java.io.File;
20  import java.net.URI;
21  import java.net.URLDecoder;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.StringTokenizer;
28  import java.util.TreeMap;
29  import java.util.concurrent.ConcurrentHashMap;
30  import java.util.concurrent.atomic.AtomicReference;
31  
32  import javax.xml.namespace.QName;
33  
34  /**
35   * This has the following logic:
36   * - if an address is specified, it is used verbatim (except for parameters); this is consistent with the generic case
37   * - otherwise, we construct from components, omitting things that aren't specified as much as possible
38   * (use required attributes to guarantee entries)
39   *
40   * In addition, parameters are handled as follows:
41   * - parameters can be given in the uri, the queryMap, or both
42   * - queryMap values override uri values
43   * - the order of parameters in the uri remains the same (even if values change)
44   * - queryMap parameters are appended after uri parameters
45   *
46   * TODO - check that we have sufficient control via XML (what about empty strings?)
47   *
48   * Not called EndpointURIBuilder because of {@link org.mule.api.endpoint.EndpointURIBuilder}
49   *
50   */
51  public class URIBuilder implements AnnotatedObject
52  {
53  
54      private static final String DOTS = ":";
55      private static final String DOTS_SLASHES = DOTS + "//";
56      private static final String QUERY = "?";
57      private static final String AND = "&";
58      private static final String EQUALS = "=";
59      private static final String BACKSLASH = "\\";
60  
61      public static final String META = "meta";
62      public static final String PROTOCOL = "protocol";
63      public static final String USER = "user";
64      public static final String PASSWORD = "password";
65      public static final String HOST = "host";
66      public static final String ADDRESS = "address";
67      public static final String PORT = "port";
68      public static final String PATH = "path";
69  
70      public static final String[] ALL_ATTRIBUTES =
71              new String[]{META, PROTOCOL, USER, PASSWORD, HOST, ADDRESS, PORT, PATH};
72      // combinations used in various endpoint parsers to validate required attributes
73      public static final String[] PATH_ATTRIBUTES = new String[]{PATH};
74      public static final String[] HOST_ATTRIBUTES = new String[]{HOST};
75      public static final String[] SOCKET_ATTRIBUTES = new String[]{HOST, PORT};
76      public static final String[] USERHOST_ATTRIBUTES = new String[]{USER, HOST};
77      // this doesn't include address, since that is handled separately (and is exclusive with these)
78      public static final String[] ALL_TRANSPORT_ATTRIBUTES = new String[]{USER, PASSWORD, HOST, PORT, PATH};
79  
80      private String address;
81      private String meta;
82      private String protocol;
83      private String user;
84      private String password;
85      private String host;
86      private Integer port;
87      private String path;
88      private Map queryMap;
89      private MuleContext muleContext;
90      private final Map<QName, Object> annotations = new ConcurrentHashMap<QName, Object>();
91  
92      private AtomicReference<EndpointURI> cache = new AtomicReference<EndpointURI>();
93  
94      public URIBuilder()
95      {
96          //default for spring. Must call setMulecontext().
97      }
98  
99      public URIBuilder(MuleContext muleContext)
100     {
101         this.muleContext = muleContext;
102     }
103 
104     public URIBuilder(EndpointURI endpointURI)
105     {
106         this(endpointURI.getMuleContext());
107         cache.set(endpointURI);
108     }
109 
110     public URIBuilder(String address, MuleContext muleContext)
111     {
112         this(muleContext);
113         // separate meta from address, if necessary
114         int dots = address.indexOf(DOTS);
115         int dotsSlashes = address.indexOf(DOTS_SLASHES);
116         if (dots > -1 && dots < dotsSlashes)
117         {
118             this.meta = address.substring(0, dots);
119             address = address.substring(dots+1);
120         }
121         this.address = address;
122     }
123 
124     public MuleContext getMuleContext()
125     {
126         return muleContext;
127     }
128 
129     public void setMuleContext(MuleContext muleContext)
130     {
131         this.muleContext = muleContext;
132     }
133 
134     public void setUser(String user)
135     {
136         assertNotUsed();
137         this.user = user;
138     }
139 
140     public void setPassword(String password)
141     {
142         assertNotUsed();
143         this.password = password;
144     }
145 
146     public void setHost(String host)
147     {
148         assertNotUsed();
149         this.host = host;
150     }
151 
152     public void setAddress(String address)
153     {
154         assertNotUsed();
155         this.address = address;
156         assertAddressConsistent();
157     }
158 
159     public void setPort(int port)
160     {
161         assertNotUsed();
162         this.port = new Integer(port);
163     }
164 
165     public void setProtocol(String protocol)
166     {
167         assertNotUsed();
168         this.protocol = protocol;
169         assertAddressConsistent();
170     }
171 
172     public void setMeta(String meta)
173     {
174         assertNotUsed();
175         this.meta = meta;
176     }
177 
178     public void setPath(String path)
179     {
180         assertNotUsed();
181         if (null != path)
182         {
183             if (path.indexOf(DOTS_SLASHES) > -1)
184             {
185                 throw new IllegalArgumentException("Unusual syntax in path: '" + path + "' contains " + DOTS_SLASHES);
186             }
187             else if (path.contains(BACKSLASH))
188             {
189                 // Windows syntax.  convert it to URI syntax
190                 try
191                 {
192                     URI pathUri = new File(path).toURI();
193                     path = pathUri.getPath();
194                 }
195                 catch (Exception ex)
196                 {
197                     throw new IllegalArgumentException("Illegal syntax in path: " + path, ex);
198                 }
199             }
200         }
201         this.path = path;
202     }
203 
204     public void setQueryMap(Map queryMap)
205     {
206         assertNotUsed();
207         this.queryMap = queryMap;
208     }
209 
210     public EndpointURI getEndpoint()
211     {
212         if (null == cache.get())
213         {
214             try
215             {
216                 EndpointURI endpointUri = new MuleEndpointURI(getConstructor(), getEncodedConstructor(), muleContext);
217                 cache.compareAndSet(null, endpointUri);
218             }
219             catch (EndpointException e)
220             {
221                 throw (IllegalStateException)new IllegalStateException("Bad endpoint configuration").initCause(e);
222             }
223         }
224         return cache.get();
225     }
226 
227     /**
228      * @return The String supplied to the delegate constructor
229      */
230     protected String getConstructor()
231     {
232         return URLDecoder.decode(getEncodedConstructor());
233     }
234 
235     protected String getEncodedConstructor()
236     {
237         StringBuffer buffer = new StringBuffer();
238         appendMeta(buffer);
239         OrderedQueryParameters uriQueries = appendAddress(buffer);
240         uriQueries.override(queryMap);
241         buffer.append(uriQueries.toString());
242         return buffer.toString();
243     }
244 
245     private void appendMeta(StringBuffer buffer)
246     {
247         if (null != meta)
248         {
249             buffer.append(meta);
250             buffer.append(DOTS);
251         }
252     }
253 
254     private OrderedQueryParameters appendAddress(StringBuffer buffer)
255     {
256         if (null != address)
257         {
258             int index = address.indexOf(QUERY);
259             if (index > -1)
260             {
261                 buffer.append(address.substring(0, index));
262                 return parseQueries(address.substring(index + 1));
263             }
264             else
265             {
266                 buffer.append(address);
267                 return new OrderedQueryParameters();
268             }
269         }
270         else
271         {
272             constructAddress(buffer);
273             return new OrderedQueryParameters();
274         }
275     }
276 
277     private OrderedQueryParameters parseQueries(String queries)
278     {
279         OrderedQueryParameters map = new OrderedQueryParameters();
280         StringTokenizer query = new StringTokenizer(queries, AND);
281         while (query.hasMoreTokens())
282         {
283             StringTokenizer nameValue = new StringTokenizer(query.nextToken(), EQUALS);
284             String name = nameValue.nextToken();
285             String value = null;
286             if (nameValue.hasMoreTokens())
287             {
288                 value = nameValue.nextToken();
289             }
290             map.put(name, value);
291         }
292         return map;
293     }
294 
295     private void constructAddress(StringBuffer buffer)
296     {
297         buffer.append(protocol);
298         buffer.append(DOTS_SLASHES);
299         boolean atStart = true;
300         if (null != user)
301         {
302             buffer.append(user);
303             if (null != password)
304             {
305                 buffer.append(":");
306                 buffer.append(password);
307             }
308             buffer.append("@");
309             atStart = false;
310         }
311         if (null != host)
312         {
313             buffer.append(host);
314             if (null != port)
315             {
316                 buffer.append(":");
317                 buffer.append(port);
318             }
319             atStart = false;
320         }
321         if (null != path)
322         {
323             if (! atStart)
324             {
325                 buffer.append("/");
326             }
327             buffer.append(path);
328         }
329     }
330 
331     protected void assertNotUsed()
332     {
333         if (null != cache.get())
334         {
335             throw new IllegalStateException("Too late to set values - builder already used");
336         }
337     }
338 
339     protected void assertAddressConsistent()
340     {
341         if (null != meta)
342         {
343             if (null != address)
344             {
345                 if (address.startsWith(meta + DOTS))
346                 {
347                     throw new IllegalArgumentException("Meta-protocol '" + meta +
348                             "' should not be specified in the address '" + address +
349                             "' - it is implicit in the element namespace.");
350                 }
351                 if (null != protocol)
352                 {
353                     assertProtocolConsistent();
354                 }
355                 else
356                 {
357                     if (address.indexOf(DOTS_SLASHES) == -1)
358                     {
359                         throw new IllegalArgumentException("Address '" + address +
360                                 "' does not have a transport protocol prefix " +
361                                 "(omit the meta protocol prefix, '" + meta + DOTS +
362                                 "' - it is implicit in the element namespace).");
363                     }
364                 }
365             }
366         }
367         else
368         {
369             assertProtocolConsistent();
370         }
371     }
372 
373     protected void assertProtocolConsistent()
374     {
375         if (null != protocol && null != address && !address.startsWith(protocol + DOTS_SLASHES))
376         {
377             throw new IllegalArgumentException("Address '" + address + "' for protocol '" + protocol +
378                     "' should start with " + protocol + DOTS_SLASHES);
379         }
380     }
381 
382     @Override
383     public String toString()
384     {
385         return getConstructor();
386     }
387 
388     @Override
389     public boolean equals(Object other)
390     {
391         if (null == other || !getClass().equals(other.getClass())) return false;
392         if (this == other) return true;
393 
394         URIBuilder builder = (URIBuilder) other;
395         return equal(address, builder.address)
396                 && equal(meta, builder.meta)
397                 && equal(protocol, builder.protocol)
398                 && equal(user, builder.user)
399                 && equal(password, builder.password)
400                 && equal(host, builder.host)
401                 && equal(port, builder.port)
402                 && equal(path, builder.path)
403                 && equal(queryMap, builder.queryMap);
404     }
405 
406     protected static boolean equal(Object a, Object b)
407     {
408         return ClassUtils.equal(a, b);
409     }
410 
411     @Override
412     public int hashCode()
413     {
414         return ClassUtils.hash(new Object[]{address, meta, protocol, user, password, host, port, path, queryMap});
415     }
416 
417     private static class OrderedQueryParameters
418     {
419         private List<String> names = new ArrayList<String>();
420         private List<String> values = new ArrayList<String>();
421 
422         public void put(String name, String value)
423         {
424             names.add(name);
425             values.add(value);
426         }
427 
428         /**
429          * Replace the first instance of the given parameter. This method does not make sense under the assumption that
430          * a given parameter name can have multiple values, so here we simply preserve the existing semantics.
431          * @param map A map off the name/value pairs to add/replace in the query string
432          */
433         public void override(Map map)
434         {
435             if (null != map)
436             {
437                 // order additional parameters
438                 Iterator mapNames = new TreeMap(map).keySet().iterator();
439                 while (mapNames.hasNext())
440                 {
441                     String name = (String) mapNames.next();
442                     String value = (String) map.get(name);
443 
444                     int pos = names.indexOf(name);
445                     if (pos >= 0)
446                     {
447                         // Found, so replace
448                         values.set(pos, value);
449                     }
450                     else
451                     {
452                         // Append new value
453                         names.add(name);
454                         values.add(value);
455                     }
456                  }
457             }
458         }
459 
460         @Override
461         public String toString()
462         {
463             StringBuffer buffer = new StringBuffer();
464 
465             boolean first = true;
466 
467             for (int i = 0; i < names.size(); i++)
468             {
469                 if (first)
470                 {
471                     buffer.append(QUERY);
472                     first = false;
473                 }
474                 else
475                 {
476                     buffer.append(AND);
477                 }
478 
479                 buffer.append(names.get(i));
480                 String value = values.get(i);
481 
482                 if (null != value)
483                 {
484                     buffer.append(EQUALS);
485                     buffer.append(value);
486                 }
487             }
488             return buffer.toString();
489         }
490     }
491 
492     public final Object getAnnotation(QName name)
493     {
494         return annotations.get(name);
495     }
496 
497     public final Map<QName, Object> getAnnotations()
498     {
499         return Collections.unmodifiableMap(annotations);
500     }
501 
502     public synchronized final void setAnnotations(Map<QName, Object> newAnnotations)
503     {
504         annotations.clear();
505         annotations.putAll(newAnnotations);
506     }
507 }