View Javadoc

1   package com.atlassian.johnson.config;
2   
3   import com.atlassian.johnson.Initable;
4   import com.atlassian.johnson.event.*;
5   import com.atlassian.johnson.setup.ContainerFactory;
6   import com.atlassian.johnson.setup.DefaultContainerFactory;
7   import com.atlassian.johnson.setup.DefaultSetupConfig;
8   import com.atlassian.johnson.setup.SetupConfig;
9   import com.atlassian.plugin.servlet.util.DefaultPathMapper;
10  import com.atlassian.plugin.servlet.util.PathMapper;
11  import com.google.common.base.Function;
12  import com.google.common.collect.ImmutableList;
13  import com.google.common.collect.ImmutableMap;
14  import com.google.common.collect.Iterables;
15  import com.opensymphony.util.ClassLoaderUtil;
16  import org.apache.commons.lang.StringUtils;
17  import org.slf4j.Logger;
18  import org.slf4j.LoggerFactory;
19  import org.w3c.dom.*;
20  import org.xml.sax.SAXException;
21  
22  import javax.xml.parsers.DocumentBuilder;
23  import javax.xml.parsers.DocumentBuilderFactory;
24  import javax.xml.parsers.ParserConfigurationException;
25  import java.io.IOException;
26  import java.lang.reflect.Constructor;
27  import java.lang.reflect.InvocationTargetException;
28  import java.lang.reflect.UndeclaredThrowableException;
29  import java.net.URL;
30  import java.util.*;
31  
32  /**
33   * Loads configuration for Johnson from an XML file.
34   *
35   * @since 2.0
36   */
37  public class XmlJohnsonConfig implements JohnsonConfig
38  {
39      public static final String DEFAULT_CONFIGURATION_FILE = "johnson-config.xml";
40  
41      private static final Logger LOG = LoggerFactory.getLogger(XmlJohnsonConfig.class);
42  
43      private final List<ApplicationEventCheck> applicationEventChecks;
44      private final ContainerFactory containerFactory;
45      private final String errorPath;
46      private final List<EventCheck> eventChecks;
47      private final Map<Integer, EventCheck> eventChecksById;
48      private final Map<String, EventLevel> eventLevels;
49      private final Map<String, EventType> eventTypes;
50      private final PathMapper ignoreMapper;
51      private final List<String> ignorePaths;
52      private final Map<String, String> params;
53      private final List<RequestEventCheck> requestEventChecks;
54      private final SetupConfig setupConfig;
55      private final String setupPath;
56  
57      private XmlJohnsonConfig(SetupConfig setupConfig, ContainerFactory containerFactory, List<EventCheck> eventChecks,
58                               Map<Integer, EventCheck> eventChecksById, Map<String, EventLevel> eventLevels,
59                               Map<String, EventType> eventTypes, List<String> ignorePaths, Map<String, String> params,
60                               String setupPath, String errorPath) {
61          this.containerFactory = containerFactory;
62          this.errorPath = errorPath;
63          this.eventChecks = eventChecks;
64          this.eventChecksById = eventChecksById;
65          this.eventLevels = eventLevels;
66          this.eventTypes = eventTypes;
67          this.ignorePaths = ignorePaths;
68          this.params = params;
69          this.setupConfig = setupConfig;
70          this.setupPath = setupPath;
71  
72          ImmutableList.Builder<ApplicationEventCheck> applicationBuilder = ImmutableList.builder();
73          ImmutableList.Builder<RequestEventCheck> requestBuilder = ImmutableList.builder();
74          for (EventCheck eventCheck : eventChecks)
75          {
76              if (eventCheck instanceof ApplicationEventCheck)
77              {
78                  applicationBuilder.add((ApplicationEventCheck) eventCheck);
79              }
80              if (eventCheck instanceof RequestEventCheck)
81              {
82                  requestBuilder.add((RequestEventCheck) eventCheck);
83              }
84          }
85          applicationEventChecks = applicationBuilder.build();
86          requestEventChecks = requestBuilder.build();
87          
88          ignoreMapper = new DefaultPathMapper();
89          ignoreMapper.put(errorPath, errorPath);
90          ignoreMapper.put(setupPath, setupPath);
91          for (String path : ignorePaths)
92          {
93              ignoreMapper.put(path, path);
94          }
95      }
96  
97      public static XmlJohnsonConfig fromDocument(Document document)
98      {
99          Element root = document.getDocumentElement();
100 
101         SetupConfig setupConfig = configureClass(root, "setup-config", SetupConfig.class, DefaultSetupConfig.class);
102         ContainerFactory containerFactory = configureClass(root, "container-factory",
103                 ContainerFactory.class, DefaultContainerFactory.class);
104         Map<String, EventLevel> eventLevels = configureEventConstants(root, "event-levels", EventLevel.class);
105         Map<String, EventType> eventTypes = configureEventConstants(root, "event-types", EventType.class);
106         Map<String, String> params = configureParameters(root);
107         String setupPath = Iterables.getOnlyElement(configurePaths(root, "setup"));
108         String errorPath = Iterables.getOnlyElement(configurePaths(root, "error"));
109         List<String> ignorePaths = configurePaths(root, "ignore");
110 
111         ElementIterable elements = getElementsByTagName(root, "event-checks");
112 
113         ArrayList<EventCheck> checks = new ArrayList<EventCheck>(elements.size());
114         Map<Integer, EventCheck> checksById = new HashMap<Integer, EventCheck>(elements.size());
115         if (!elements.isEmpty())
116         {
117             elements = getElementsByTagName(Iterables.getOnlyElement(elements), "event-check");
118             for (Element element : elements)
119             {
120                 EventCheck check = parseEventCheck(element);
121                 checks.add(check);
122 
123                 String id = element.getAttribute("id");
124                 if (StringUtils.isNotBlank(id))
125                 {
126                     try
127                     {
128                         if (checksById.put(Integer.parseInt(id), check) != null)
129                         {
130                             throw new ConfigurationJohnsonException("EventCheck ID [" + id + "] is not unique");
131                         }
132                     }
133                     catch (NumberFormatException e)
134                     {
135                         throw new ConfigurationJohnsonException("EventCheck ID [" + id + "] is not a number", e);
136                     }
137                 }
138             }
139         }
140 
141         return new XmlJohnsonConfig(setupConfig, containerFactory, ImmutableList.copyOf(checks),
142                 ImmutableMap.copyOf(checksById), eventLevels, eventTypes, ignorePaths, params, setupPath, errorPath);
143     }
144 
145     public static XmlJohnsonConfig fromFile(String fileName)
146     {
147         URL url = ClassLoaderUtil.getResource(fileName, XmlJohnsonConfig.class);
148         if (url != null)
149         {
150             LOG.debug("Loading {} from classpath at {}", fileName, url);
151             fileName = url.toString();
152         }
153 
154         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
155         try
156         {
157             DocumentBuilder builder = factory.newDocumentBuilder();
158             Document document = builder.parse(fileName);
159             
160             return fromDocument(document);
161         }
162         catch (IOException e)
163         {
164             throw new ConfigurationJohnsonException("Failed to parse [" + fileName + "]; the file could not be read", e);
165         }
166         catch (ParserConfigurationException e)
167         {
168             throw new ConfigurationJohnsonException("Failed to parse [" + fileName + "]; JVM configuration is invalid", e);
169         }
170         catch (SAXException e)
171         {
172             throw new ConfigurationJohnsonException("Failed to parse [" + fileName + "]; XML is not well-formed", e);
173         }
174     }
175 
176     public List<ApplicationEventCheck> getApplicationEventChecks()
177     {
178         return applicationEventChecks;
179     }
180 
181     public ContainerFactory getContainerFactory()
182     {
183         return containerFactory;
184     }
185 
186     public String getErrorPath()
187     {
188         return errorPath;
189     }
190 
191     public EventCheck getEventCheck(int id)
192     {
193         return eventChecksById.get(id);
194     }
195 
196     public List<EventCheck> getEventChecks()
197     {
198         return eventChecks;
199     }
200 
201     public EventLevel getEventLevel(String level)
202     {
203         return eventLevels.get(level);
204     }
205 
206     public EventType getEventType(String type)
207     {
208         return eventTypes.get(type);
209     }
210 
211     public List<String> getIgnorePaths()
212     {
213         return ignorePaths;
214     }
215 
216     public Map<String, String> getParams()
217     {
218         return params;
219     }
220 
221     public List<RequestEventCheck> getRequestEventChecks()
222     {
223         return requestEventChecks;
224     }
225 
226     public SetupConfig getSetupConfig()
227     {
228         return setupConfig;
229     }
230 
231     public String getSetupPath()
232     {
233         return setupPath;
234     }
235 
236     public boolean isIgnoredPath(String uri)
237     {
238         return ignoreMapper.get(uri) != null;
239     }
240 
241     private static <T> Map<String, T> configureEventConstants(Element root, String tagName, Class<T> childClass)
242     {
243         Constructor<T> constructor;
244         try
245         {
246             constructor = childClass.getConstructor(String.class, String.class);
247         }
248         catch (NoSuchMethodException e)
249         {
250             throw new IllegalArgumentException("Class [" + childClass.getName() +
251                     "] requires a String, String constructor");
252         }
253 
254         ElementIterable elements = getElementsByTagName(root, tagName);
255         if (elements.isEmpty())
256         {
257             return Collections.emptyMap();
258         }
259         elements = getElementsByTagName(Iterables.getOnlyElement(elements), tagName.substring(0, tagName.length() - 1));
260 
261         ImmutableMap.Builder<String, T> builder = ImmutableMap.builder();
262         for (Element element : elements)
263         {
264             String key = element.getAttribute("key");
265             String description = getContainedText(element, "description");
266 
267             try
268             {
269                 builder.put(key, constructor.newInstance(key, description));
270             }
271             catch (IllegalAccessException e)
272             {
273                 throw new IllegalArgumentException("Constructor [" + constructor.getName() + "] must be public");
274             }
275             catch (InstantiationException e)
276             {
277                 throw new IllegalArgumentException("Class [" + childClass.getName() + "] may not be abstract");
278             }
279             catch (InvocationTargetException e)
280             {
281                 Throwable cause = e.getCause();
282                 if (cause instanceof RuntimeException)
283                 {
284                     throw (RuntimeException) cause;
285                 }
286                 throw new UndeclaredThrowableException(cause);
287             }
288         }
289         return builder.build();
290     }
291 
292     private static List<String> configurePaths(Element root, String tagname)
293     {
294         ElementIterable elements = getElementsByTagName(root, tagname);
295         if (elements.isEmpty())
296         {
297             return Collections.emptyList();
298         }
299         elements = getElementsByTagName(Iterables.getOnlyElement(elements), "path");
300 
301         return ImmutableList.copyOf(
302                 Iterables.transform(elements, new Function<Element, String>()
303                 {
304                     @Override
305                     public String apply(Element input)
306                     {
307                         return ((Text) input.getFirstChild()).getData().trim();
308                     }
309                 }));
310     }
311 
312     private static Map<String, String> configureParameters(Element root)
313     {
314         NodeList list = root.getElementsByTagName("parameters");
315         if (isEmpty(list))
316         {
317             return Collections.emptyMap();
318         }
319 
320         Element element = (Element) list.item(0);
321         return getInitParameters(element);
322     }
323 
324     private static <T> T configureClass(Element root, String tagname, Class<T> expectedClass, Class<? extends T> defaultClass)
325     {
326         ElementIterable elements = getElementsByTagName(root, tagname);
327         if (elements.isEmpty())
328         {
329             try
330             {
331                 return defaultClass.newInstance();
332             }
333             catch (Exception e)
334             {
335                 throw new ConfigurationJohnsonException("Default [" + expectedClass.getName() + "], [" +
336                         defaultClass.getName() + "] is not valid", e);
337             }
338         }
339 
340         Element element = Iterables.getOnlyElement(elements);
341         String className = element.getAttribute("class");
342         try
343         {
344             Class<?> clazz = ClassLoaderUtil.loadClass(className, XmlJohnsonConfig.class);
345             if (!expectedClass.isAssignableFrom(clazz))
346             {
347                 throw new ConfigurationJohnsonException("The class specified by " + tagname + " (" + className +
348                         ") is required to implement [" + expectedClass.getName() + "]");
349             }
350 
351             T instance = expectedClass.cast(clazz.newInstance());
352             if (instance instanceof Initable)
353             {
354                 Map<String, String> params = getInitParameters(element);
355                 ((Initable) instance).init(params);
356             }
357             return instance;
358         }
359         catch (Exception e)
360         {
361             throw new ConfigurationJohnsonException("Could not create: " + tagname, e);
362         }
363     }
364 
365     private static Map<String, String> getInitParameters(Element root)
366     {
367         ElementIterable elements = new ElementIterable(root.getElementsByTagName("init-param"));
368 
369         ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
370         for (Element element : elements)
371         {
372             String paramName = getContainedText(element, "param-name");
373             String paramValue = getContainedText(element, "param-value");
374             builder.put(paramName, paramValue);
375         }
376         return builder.build();
377     }
378 
379     private static String getContainedText(Node parent, String childTagName)
380     {
381         try
382         {
383             Node tag = ((Element) parent).getElementsByTagName(childTagName).item(0);
384             return ((Text) tag.getFirstChild()).getData();
385         }
386         catch (Exception e)
387         {
388             return null;
389         }
390     }
391     
392     private static ElementIterable getElementsByTagName(Node parent, String tagName)
393     {
394         Element element = (Element) parent;
395         NodeList list = element.getElementsByTagName(tagName);
396         if (isEmpty(list) && tagName.contains("-"))
397         {
398             //Many tags used to not have hyphens, so we fall back to the old run-together approach
399             list = element.getElementsByTagName(tagName.replace("-", ""));
400         }
401 
402         return new ElementIterable(list);
403     }
404 
405     private static boolean isEmpty(NodeList list)
406     {
407         return (list == null || list.getLength() == 0);
408     }
409 
410     private static EventCheck parseEventCheck(Element element)
411     {
412         String className = element.getAttribute("class");
413         if (StringUtils.isBlank(className))
414         {
415             throw new ConfigurationJohnsonException("event-check element with bad class attribute");
416         }
417 
418         Object o;
419         try
420         {
421             LOG.trace("Loading class [{}]", className);
422             Class eventCheckClazz = ClassLoaderUtil.loadClass(className, XmlJohnsonConfig.class);
423             LOG.trace("Instantiating [{}]", className);
424             o = eventCheckClazz.newInstance();
425         }
426         catch (ClassNotFoundException e)
427         {
428             LOG.error("Failed to load EventCheck class [" + className + "]", e);
429             throw new ConfigurationJohnsonException("Could not load EventCheck: " + className, e);
430         }
431         catch (IllegalAccessException e)
432         {
433             LOG.error("Missing public nullary constructor for EventCheck class [" + className + "]", e);
434             throw new ConfigurationJohnsonException("Could not instantiate EventCheck: " + className, e);
435         }
436         catch (InstantiationException e)
437         {
438             LOG.error("Could not instantiate EventCheck class [" + className + "]", e);
439             throw new ConfigurationJohnsonException("Could not instantiate EventCheck: " + className, e);
440         }
441 
442         if (!(o instanceof EventCheck))
443         {
444             throw new ConfigurationJohnsonException(className + " does not implement EventCheck");
445         }
446 
447         LOG.debug("Adding EventCheck of class: " + className);
448         EventCheck eventCheck = (EventCheck) o;
449         if (eventCheck instanceof Initable)
450         {
451             ((Initable) eventCheck).init(getInitParameters(element));
452         }
453         return eventCheck;
454     }
455 
456     private static class ElementIterable implements Iterable<Element> {
457 
458         private final NodeList list;
459 
460         private ElementIterable(NodeList list) {
461             this.list = list;
462         }
463 
464         @Override
465         public Iterator<Element> iterator() {
466             return new Iterator<Element>() {
467                 private int index;
468 
469                 public boolean hasNext() {
470                     return index < list.getLength();
471                 }
472 
473                 public Element next() {
474                     if (hasNext()) {
475                         return (Element) list.item(index++);
476                     }
477                     throw new NoSuchElementException();
478                 }
479 
480                 public void remove() {
481                     throw new UnsupportedOperationException();
482                 }
483             };
484         }
485 
486         public boolean isEmpty() {
487             return (list == null || list.getLength() == 0);
488         }
489         
490         public int size() {
491             return (list == null ? 0 : list.getLength());
492         }
493     }
494 }