View Javadoc

1   package com.atlassian.plugins.rest.module.filter;
2   
3   import java.text.ParseException;
4   import java.util.ArrayList;
5   import java.util.Collections;
6   import java.util.Comparator;
7   import java.util.List;
8   import java.util.Locale;
9   
10  import javax.ws.rs.WebApplicationException;
11  import javax.ws.rs.core.HttpHeaders;
12  import javax.ws.rs.core.Response;
13  import javax.ws.rs.ext.Provider;
14  
15  import com.sun.jersey.core.header.InBoundHeaders;
16  import com.sun.jersey.core.header.LanguageTag;
17  import com.sun.jersey.core.header.QualityFactor;
18  import com.sun.jersey.core.header.reader.HttpHeaderReader;
19  import com.sun.jersey.spi.container.AdaptingContainerRequest;
20  import com.sun.jersey.spi.container.ContainerRequest;
21  import com.sun.jersey.spi.container.ContainerRequestFilter;
22  
23  /**
24   * Filter wraps ContainerRequest to change the way how AcceptableLanguage header parsed
25   *
26   * By default Jersey doesn't process well UN M.49 region codes. Filter fixes that by wrapping
27   * ContainerRequest and replace method that parses AcceptableLanguage header
28   *
29   * @author pandronov
30   */
31  @Provider
32  public class AcceptLanguageFilter implements ContainerRequestFilter {
33      // HttpHeaderReader#QUALITY_COMPARATOR direct copy (private scope... :()
34      private static final Comparator<QualityFactor> QUALITY_COMPARATOR = new Comparator<QualityFactor>() {
35          public int compare(QualityFactor o1, QualityFactor o2) {
36              return o2.getQuality() - o1.getQuality();
37          }
38      };
39  
40      private static final CustomLanguageTag ANY_LANG = new CustomLanguageTag("*", null);
41  
42      @Override
43      public ContainerRequest filter(ContainerRequest request) {
44          return new AdaptingContainerRequest(request) {
45  
46              private List<Locale> acceptLanguages;
47  
48              @Override
49              public void setHeaders(InBoundHeaders headers) {
50                  // As usual jersey hides everything in private scope, so
51                  // nice headersModCount is not available :(
52                  super.setHeaders(headers);
53                  acceptLanguages = null;
54              }
55  
56              @Override
57              public List<Locale> getAcceptableLanguages() {
58                  if (acceptLanguages == null) {
59                      List<CustomLanguageTag> alts = parseAcceptLanguage();
60  
61                      acceptLanguages = new ArrayList<Locale>(alts.size());
62                      for (CustomLanguageTag alt : alts) {
63                          acceptLanguages.add(alt.getAsLocale());
64                      }
65                  }
66  
67                  return acceptLanguages;
68              }
69  
70              // Copy of HttpHelper & HttpHeaderReader code adjusted to accept
71              // UN M.49 region codes
72              private List<CustomLanguageTag> parseAcceptLanguage() {
73                  final String acceptLanguage = getHeaderValue(HttpHeaders.ACCEPT_LANGUAGE);
74                  if (acceptLanguage == null || acceptLanguage.length() == 0) {
75                      return Collections.singletonList(ANY_LANG);
76                  }
77  
78                  try {
79                      List<CustomLanguageTag> result = new ArrayList<>();
80                      HttpHeaderReader reader = HttpHeaderReader.newInstance(acceptLanguage);
81                      HttpHeaderListAdapter adapter = new HttpHeaderListAdapter(reader);
82                      while (reader.hasNext()) {
83                          result.add(parserLanguageTag(adapter));
84                          adapter.reset();
85  
86                          if (reader.hasNext()) {
87                              reader.next();
88                          }
89                      }
90  
91                      Collections.sort(result, QUALITY_COMPARATOR);
92                      return result;
93                  } catch (java.text.ParseException e) {
94                      throw new WebApplicationException(
95                              e,
96                              Response.status(Response.Status.BAD_REQUEST)
97                                      .entity("Bad Accept-Language header value: '" + acceptLanguage + "'")
98                                      .type("text/plain")
99                                      .build()
100                     );
101                 }
102             }
103 
104             // Parse core from LanguageTag 
105             private CustomLanguageTag parserLanguageTag(HttpHeaderReader reader) throws ParseException {
106                 // Skip any white space
107                 reader.hasNext();
108                 String primaryTag = null, subTags = null, languageTag = reader.nextToken();
109 
110                 if (!languageTag.equals("*")) {
111                     if (!isLanguageTagValid(languageTag)) {
112                         throw new ParseException("String, " + languageTag + ", is not a valid language tag", 0);
113                     }
114 
115                     final int index = languageTag.indexOf('-');
116                     if (index == -1) {
117                         primaryTag = languageTag;
118                         subTags = null;
119                     } else {
120                         primaryTag = languageTag.substring(0, index);
121                         subTags = languageTag.substring(index + 1, languageTag.length());
122                     }
123                 } else {
124                     primaryTag = languageTag;
125                 }
126 
127                 int quality;
128                 if (reader.hasNext()) {
129                     quality = HttpHeaderReader.readQualityFactorParameter(reader);
130                 } else {
131                     quality = CustomLanguageTag.DEFAULT_QUALITY_FACTOR;
132                 }
133 
134                 return new CustomLanguageTag(languageTag, primaryTag, subTags, quality);
135             }
136 
137             // Parse core from LanguageTag, modified to accept UN M.49 codes 
138             private boolean isLanguageTagValid(String tag) {
139                 int alphaCount = 0, parts = 0;
140                 for (int i = 0; i < tag.length(); i++) {
141                     final char c = tag.charAt(i);
142                     if (c == '-') {
143                         if (alphaCount == 0) {
144                             return false;
145                         }
146                         alphaCount = 0;
147                         parts++;
148                     } else if (
149                             ('A' <= c && c <= 'Z') ||
150                                     ('a' <= c && c <= 'z') ||
151                                     (Character.isDigit(c) && (parts > 0 || alphaCount > 0))
152                             ) {
153                         alphaCount++;
154                         if (alphaCount > 8)
155                             return false;
156                     } else {
157                         return false;
158                     }
159                 }
160                 return (alphaCount != 0);
161             }
162         };
163     }
164 
165     // Copy of LanguageTag & AcceptableLanguageTag - private scope & final methods.... :(
166     private static final class CustomLanguageTag implements QualityFactor {
167 
168         protected int quality = DEFAULT_QUALITY_FACTOR;
169 
170         protected String tag;
171 
172         protected String primaryTag;
173 
174         protected String subTags;
175 
176         public CustomLanguageTag(String primaryTag, String subTags) {
177             this(
178                     (subTags != null && subTags.length() > 0 ? primaryTag + "-" + subTags : primaryTag),
179                     primaryTag,
180                     subTags,
181                     DEFAULT_QUALITY_FACTOR
182             );
183         }
184 
185         public CustomLanguageTag(String tag, String primaryTag, String subTags, int quality) {
186             this.tag = tag;
187             this.primaryTag = primaryTag;
188             this.subTags = subTags;
189             this.quality = quality;
190         }
191 
192         public final Locale getAsLocale() {
193             return (subTags == null)
194                     ? new Locale(primaryTag)
195                     : new Locale(primaryTag, subTags);
196         }
197 
198         @Override
199         public int getQuality() {
200             return quality;
201         }
202 
203         @Override
204         public boolean equals(Object object) {
205             if (object instanceof LanguageTag) {
206                 LanguageTag lt = (LanguageTag) object;
207 
208                 if (this.tag != null)
209                     if (!this.tag.equals(lt.getTag()))
210                         return false;
211                     else if (lt.getTag() != null)
212                         return false;
213 
214                 if (this.primaryTag != null)
215                     if (!this.primaryTag.equals(lt.getPrimaryTag()))
216                         return false;
217                     else if (lt.getPrimaryTag() != null)
218                         return false;
219 
220                 if (this.subTags != null)
221                     if (!this.subTags.equals(lt.getSubTags()))
222                         return false;
223                     else if (lt.getSubTags() != null)
224                         return false;
225 
226                 return true;
227             } else {
228                 return false;
229             }
230         }
231 
232         @Override
233         public int hashCode() {
234             return (tag == null ? 0 : tag.hashCode()) +
235                     (primaryTag == null ? 0 : primaryTag.hashCode()) +
236                     (subTags == null ? 0 : primaryTag.hashCode());
237         }
238 
239         @Override
240         public String toString() {
241             return primaryTag + (subTags == null ? "" : subTags);
242         }
243     }
244 }