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