View Javadoc

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