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
34
35
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
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 }