001package org.javasimon;
002
003import java.io.IOException;
004import java.io.Reader;
005import java.lang.reflect.InvocationTargetException;
006import java.util.ArrayList;
007import java.util.LinkedHashMap;
008import java.util.List;
009import java.util.Map;
010
011import javax.xml.stream.XMLInputFactory;
012import javax.xml.stream.XMLStreamException;
013import javax.xml.stream.XMLStreamReader;
014
015import org.javasimon.callback.Callback;
016import org.javasimon.callback.CompositeCallback;
017import org.javasimon.callback.CompositeCallbackImpl;
018import org.javasimon.callback.CompositeFilterCallback;
019import org.javasimon.callback.FilterCallback;
020import org.javasimon.callback.FilterRule;
021import org.javasimon.utils.bean.SimonBeanUtils;
022
023/**
024 * Holds configuration for one Simon Manager. Configuration is read from the stream
025 * and it is merged with any existing configuration read before. Method {@link #clear()}
026 * must be used in order to reset this configuration object.
027 * <p/>
028 * Every {@link org.javasimon.Manager} holds its own configuration and programmer has
029 * to take care of the initialization of the configuration. Default {@link org.javasimon.SimonManager}
030 * is privileged and can be configured via file or resource when Java property {@code javasimon.config.file}
031 * (constant {@link org.javasimon.SimonManager#PROPERTY_CONFIG_FILE_NAME})
032 * or {@code javasimon.config.resource} (constant
033 * {@link org.javasimon.SimonManager#PROPERTY_CONFIG_RESOURCE_NAME}) is used.
034 * <p/>
035 * <b>Structure of the configuration XML:</b>
036 * <pre>{@code
037 * <simon-configuration>
038 * ... TODO
039 * </simon-configuration>}</pre>
040 *
041 * @author <a href="mailto:virgo47@gmail.com">Richard "Virgo" Richter</a>
042 */
043// TODO: This class needs serious rethinking, also manager config itself should be independent from the reading
044// of the config - so it can be set up by Spring for instance
045public final class ManagerConfiguration {
046
047        private Map<SimonPattern, SimonConfiguration> configs;
048
049        private final Manager manager;
050
051        /**
052         * Creates manager configuration for a specified manager.
053         *
054         * @param manager manager on whose behalf this configuration is created
055         */
056        ManagerConfiguration(Manager manager) {
057                this.manager = manager;
058                clear();
059        }
060
061        /** Clears any previously loaded configuration. */
062        public void clear() {
063                configs = new LinkedHashMap<>();
064        }
065
066        /**
067         * Reads config from provided buffered reader. Reader is not closed after this method finishes.
068         *
069         * @param reader reader containing configuration
070         * @throws IOException thrown if problem occurs while reading from the reader
071         */
072        public void readConfig(Reader reader) throws IOException {
073                try {
074                        XMLStreamReader xr = XMLInputFactory.newInstance().createXMLStreamReader(reader);
075                        try {
076                                while (!xr.isStartElement()) {
077                                        xr.next();
078                                }
079                                processStartElement(xr, "simon-configuration");
080                                while (true) {
081                                        if (isStartTag(xr, "callback")) {
082                                                manager.callback().addCallback(processCallback(xr));
083                                        } else if (isStartTag(xr, "filter-callback")) {
084                                                manager.callback().addCallback(processFilterCallback(xr));
085                                        } else if (isStartTag(xr, "simon")) {
086                                                processSimon(xr);
087                                        } else {
088                                                break;
089                                        }
090                                }
091                                assertEndTag(xr, "simon-configuration");
092                        } finally {
093                                xr.close();
094                        }
095                } catch (XMLStreamException e) {
096                        manager.callback().onManagerWarning(null, e);
097                } catch (SimonException e) {
098                        manager.callback().onManagerWarning(e.getMessage(), e);
099                }
100        }
101
102        private Callback processCallback(XMLStreamReader xr) throws XMLStreamException {
103                Map<String, String> attrs = processStartElement(xr, "callback");
104                String klass = attrs.get("class");
105                if (klass == null) {
106                        klass = CompositeCallbackImpl.class.getName();
107                }
108                Callback callback;
109                try {
110                        callback = (Callback) Class.forName(klass).newInstance();
111                } catch (InstantiationException | ClassCastException | ClassNotFoundException | IllegalAccessException e) {
112                        throw new SimonException(e);
113                }
114
115                processSetAndCallbacks(xr, callback);
116                processEndElement(xr, "callback");
117                return callback;
118        }
119
120        private Callback processFilterCallback(XMLStreamReader xr) throws XMLStreamException {
121                Map<String, String> attrs = processStartElement(xr, "filter-callback");
122                String klass = attrs.get("class");
123                if (klass == null) {
124                        klass = CompositeFilterCallback.class.getName();
125                }
126                FilterCallback callback;
127                try {
128                        callback = (FilterCallback) Class.forName(klass).newInstance();
129                } catch (InstantiationException | ClassCastException | ClassNotFoundException | IllegalAccessException e) {
130                        throw new SimonException(e);
131                }
132
133                while (isStartTag(xr, "rule")) {
134                        processRule(xr, callback);
135                }
136                processSetAndCallbacks(xr, callback);
137                processEndElement(xr, "filter-callback");
138                return callback;
139        }
140
141        private void processSetAndCallbacks(XMLStreamReader xr, Callback callback) throws XMLStreamException {
142                while (isStartTag(xr, "set")) {
143                        processSet(xr, callback);
144                }
145                while (true) {
146                        if (isStartTag(xr, "callback")) {
147                                ((CompositeCallback) callback).addCallback(processCallback(xr));
148                        } else if (isStartTag(xr, "filter-callback")) {
149                                ((CompositeCallback) callback).addCallback(processFilterCallback(xr));
150                        } else {
151                                break;
152                        }
153                }
154        }
155
156        private void processRule(XMLStreamReader xr, FilterCallback callback) throws XMLStreamException {
157                String pattern = null;
158                FilterRule.Type type = FilterRule.Type.SUFFICE;
159                String condition = null;
160                List<Callback.Event> events = new ArrayList<>();
161
162                Map<String, String> attrs = processStartElement(xr, "rule");
163                if (attrs.get("condition") != null) {
164                        condition = attrs.get("condition");
165                }
166                if (attrs.get("type") != null) {
167                        type = FilterRule.Type.valueOf(toEnum(attrs.get("type")));
168                }
169                if (attrs.get("pattern") != null) {
170                        pattern = attrs.get("pattern");
171                }
172                if (attrs.get("events") != null) {
173                        String[] sa = attrs.get("events").trim().split(" *, *");
174                        for (String eventName : sa) {
175                                events.add(Callback.Event.forCode(eventName));
176                        }
177                }
178                if (isStartTag(xr, "condition")) {
179                        xr.next();
180                        condition = getText(xr);
181                        processEndElement(xr, "condition");
182                }
183                processEndElement(xr, "rule");
184                callback.addRule(type, condition, pattern, events.toArray(new Callback.Event[events.size()]));
185        }
186
187        private void processSet(XMLStreamReader xr, Callback callback) throws XMLStreamException {
188                Map<String, String> attrs = processStartElement(xr, "set", "property");
189                setProperty(callback, attrs.get("property"), attrs.get("value"));
190                processEndElement(xr, "set");
191        }
192
193        /**
194         * Sets the callback property.
195         *
196         * @param callback callback object
197         * @param property name of the property
198         * @param value value of the property
199         */
200        private void setProperty(Callback callback, String property, String value) {
201                try {
202                        if (value != null) {
203                                SimonBeanUtils.getInstance().setProperty(callback, property, value);
204                        } else {
205                                callback.getClass().getMethod(setterName(property)).invoke(callback);
206                        }
207                } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
208                        throw new SimonException(e);
209                }
210        }
211
212        private String setterName(String name) {
213                return "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
214        }
215
216        private void processSimon(XMLStreamReader xr) throws XMLStreamException {
217                Map<String, String> attrs = processStartElement(xr, "simon", "pattern");
218                String pattern = attrs.get("pattern");
219                SimonState state = attrs.get("state") != null ? SimonState.valueOf(toEnum(attrs.get("state"))) : null;
220                configs.put(new SimonPattern(pattern), new SimonConfiguration(state));
221                processEndElement(xr, "simon");
222        }
223
224        /**
225         * Returns configuration for the Simon with the specified name.
226         *
227         * @param name Simon name
228         * @return configuration for that particular Simon
229         */
230        SimonConfiguration getConfig(String name) {
231                SimonState state = null;
232
233                for (SimonPattern pattern : configs.keySet()) {
234                        if (pattern.matches(name)) {
235                                SimonConfiguration config = configs.get(pattern);
236                                if (config.getState() != null) {
237                                        state = config.getState();
238                                }
239                        }
240                }
241                return new SimonConfiguration(state);
242        }
243
244        private String toEnum(String enumVal) {
245                return enumVal.trim().toUpperCase().replace('-', '_');
246        }
247
248        // XML Utils
249
250        private Map<String, String> processStartElement(XMLStreamReader reader, String elementName, String... requiredAttributes) throws XMLStreamException {
251                Map<String, String> attrs = processStartElementPrivate(reader, elementName, requiredAttributes);
252                reader.nextTag();
253                return attrs;
254        }
255
256        private Map<String, String> processStartElementPrivate(XMLStreamReader reader, String elementName, String... requiredAttributes) throws XMLStreamException {
257                assertStartTag(reader, elementName);
258                Map<String, String> attrs = readAttributes(reader);
259                for (String attr : requiredAttributes) {
260                        if (!attrs.containsKey(attr)) {
261                                throw new XMLStreamException("Attribute '" + attr + "' MUST be present (element: " + elementName + "). " + readerPosition(reader));
262                        }
263                }
264                return attrs;
265        }
266
267        private void assertStartTag(XMLStreamReader reader, String name) throws XMLStreamException {
268                if (!reader.isStartElement()) {
269                        throw new XMLStreamException("Assert start tag - wrong event type " + reader.getEventType() + " (expected name: " + name + ") " + readerPosition(reader));
270                }
271                assertName(reader, "start tag", name);
272        }
273
274        private Map<String, String> readAttributes(XMLStreamReader reader) {
275                Map<String, String> attributes = new LinkedHashMap<>();
276                int attrCount = reader.getAttributeCount();
277                for (int i = 0; i < attrCount; i++) {
278                        attributes.put(reader.getAttributeName(i).toString(), reader.getAttributeValue(i));
279                }
280                return attributes;
281        }
282
283        private void assertName(XMLStreamReader reader, String operation, String name) throws XMLStreamException {
284                if (!reader.getLocalName().equals(name)) {
285                        throw new XMLStreamException("Assert " + operation + " - wrong element name " + reader.getName().toString() + " (expected name: " + name + ") " + readerPosition(reader));
286                }
287        }
288
289        private String readerPosition(XMLStreamReader reader) {
290                return "[line: " + reader.getLocation().getLineNumber() + ", column: " + reader.getLocation().getColumnNumber() + "]";
291        }
292
293        private void assertEndTag(XMLStreamReader reader, String name) throws XMLStreamException {
294                if (!reader.isEndElement()) {
295                        throw new XMLStreamException("Assert end tag - wrong event type " + reader.getEventType() + " (expected name: " + name + ") " + readerPosition(reader));
296                }
297                assertName(reader, "end tag", name);
298        }
299
300        private boolean isStartTag(XMLStreamReader reader, String name) {
301                return reader.isStartElement() && reader.getLocalName().equals(name);
302        }
303
304        private void processEndElement(XMLStreamReader reader, String name) throws XMLStreamException {
305                assertEndTag(reader, name);
306                reader.nextTag();
307        }
308
309        private String getText(XMLStreamReader reader) throws XMLStreamException {
310                StringBuilder sb = new StringBuilder();
311                while (reader.isCharacters()) {
312                        sb.append(reader.getText());
313                        reader.next();
314                }
315                return sb.toString().trim();
316        }
317}