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