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
51
52
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
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 }