001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v1.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.core.joran.action;
015
016import java.io.File;
017import java.io.IOException;
018import java.io.InputStream;
019import java.net.MalformedURLException;
020import java.net.URI;
021import java.net.URL;
022import java.util.List;
023
024import org.xml.sax.Attributes;
025
026import ch.qos.logback.core.joran.event.SaxEvent;
027import ch.qos.logback.core.joran.event.SaxEventRecorder;
028import ch.qos.logback.core.joran.spi.ActionException;
029import ch.qos.logback.core.joran.spi.JoranException;
030import ch.qos.logback.core.joran.spi.SaxEventInterpretationContext;
031import ch.qos.logback.core.joran.util.ConfigurationWatchListUtil;
032import ch.qos.logback.core.model.IncludeModel;
033import ch.qos.logback.core.model.Model;
034import ch.qos.logback.core.util.Loader;
035import ch.qos.logback.core.util.OptionHelper;
036
037import static ch.qos.logback.core.joran.JoranConstants.INCLUDED_TAG;
038
039/**
040 * 
041 * @author ceki
042 *
043 */
044public class IncludeAction extends Action {
045
046    private static final String FILE_ATTR = "file";
047    private static final String URL_ATTR = "url";
048    private static final String RESOURCE_ATTR = "resource";
049    private static final String OPTIONAL_ATTR = "optional";
050
051    private String attributeInUse;
052    private boolean optional;
053
054    Model parentModel;
055    IncludeModel includeModel;
056    boolean inError = false;
057    
058    @Override
059    public void begin(SaxEventInterpretationContext ec, String name, Attributes attributes) throws ActionException {
060
061        parentModel = null;
062        includeModel = null;
063        
064        SaxEventRecorder recorder = new SaxEventRecorder(context);
065        
066        String optionalStr = attributes.getValue(OPTIONAL_ATTR);
067        
068        createModelForAlternateUse(ec, name, attributes, optionalStr);
069        
070        
071        this.attributeInUse = null;
072        this.optional = OptionHelper.toBoolean(optionalStr, false);
073        
074        if (!checkAttributes(attributes)) {
075            inError = true;
076            return;
077        }
078         
079        InputStream in = getInputStream(ec, attributes);
080
081        try {
082            if (in != null) {
083                parseAndRecord(in, recorder);
084                // remove the <included> tag from the beginning and </included> from the end
085                trimHeadAndTail(recorder);
086
087                // offset = 2, because we need to get past this element as well as the end
088                // element
089                ec.getSaxEventInterpreter().getEventPlayer().addEventsDynamically(recorder.getSaxEventList(), 2);
090            }
091        } catch (JoranException e) {
092            addError("Error while parsing  " + attributeInUse, e);
093        } finally {
094            close(in);
095        }
096
097    }
098
099    // model created for later use, not necessarily for configuration purposes.
100    private void createModelForAlternateUse(SaxEventInterpretationContext seic, String name, Attributes attributes,
101            String optionalStr) {
102        this.includeModel = new IncludeModel();
103        this.includeModel.setOptional(optionalStr);
104        fillInIncludeModelAttributes(includeModel, name, attributes);
105        if (!seic.isModelStackEmpty()) {
106            parentModel = seic.peekModel();
107        }
108        final int lineNumber = getLineNumber(seic);
109        this.includeModel.setLineNumber(lineNumber);
110        seic.pushModel(includeModel);
111    }
112
113    private void fillInIncludeModelAttributes(IncludeModel includeModel, String name, Attributes attributes) {
114        this.includeModel.setTag(name);
115        String fileAttribute = attributes.getValue(FILE_ATTR);
116        String urlAttribute = attributes.getValue(URL_ATTR);
117        String resourceAttribute = attributes.getValue(RESOURCE_ATTR);
118        
119        this.includeModel.setFile(fileAttribute);
120        this.includeModel.setUrl(urlAttribute);
121        this.includeModel.setResource(resourceAttribute);
122        
123    }
124
125    void close(InputStream in) {
126        if (in != null) {
127            try {
128                in.close();
129            } catch (IOException e) {
130            }
131        }
132    }
133
134    private boolean checkAttributes(Attributes attributes) {
135        String fileAttribute = attributes.getValue(FILE_ATTR);
136        String urlAttribute = attributes.getValue(URL_ATTR);
137        String resourceAttribute = attributes.getValue(RESOURCE_ATTR);
138
139        int count = 0;
140
141        if (!OptionHelper.isNullOrEmpty(fileAttribute)) {
142            count++;
143        }
144        if (!OptionHelper.isNullOrEmpty(urlAttribute)) {
145            count++;
146        }
147        if (!OptionHelper.isNullOrEmpty(resourceAttribute)) {
148            count++;
149        }
150
151        if (count == 0) {
152            addError("One of \"path\", \"resource\" or \"url\" attributes must be set.");
153            return false;
154        } else if (count > 1) {
155            addError("Only one of \"file\", \"url\" or \"resource\" attributes should be set.");
156            return false;
157        } else if (count == 1) {
158            return true;
159        }
160        throw new IllegalStateException("Count value [" + count + "] is not expected");
161    }
162
163    URL attributeToURL(String urlAttribute) {
164        try {
165            return new URL(urlAttribute);
166        } catch (MalformedURLException mue) {
167            String errMsg = "URL [" + urlAttribute + "] is not well formed.";
168            addError(errMsg, mue);
169            return null;
170        }
171    }
172
173    InputStream openURL(URL url) {
174        try {
175            return url.openStream();
176        } catch (IOException e) {
177            optionalWarning("Failed to open [" + url.toString() + "]");
178            return null;
179        }
180    }
181
182    URL resourceAsURL(String resourceAttribute) {
183        URL url = Loader.getResourceBySelfClassLoader(resourceAttribute);
184        if (url == null) {
185            optionalWarning("Could not find resource corresponding to [" + resourceAttribute + "]");
186            return null;
187        } else
188            return url;
189    }
190
191    private void optionalWarning(String msg) {
192        if (!optional) {
193            addWarn(msg);
194        }
195    }
196
197    URL filePathAsURL(String path) {
198        URI uri = new File(path).toURI();
199        try {
200            return uri.toURL();
201        } catch (MalformedURLException e) {
202            // impossible to get here
203            e.printStackTrace();
204            return null;
205        }
206    }
207
208    URL getInputURL(SaxEventInterpretationContext ec, Attributes attributes) {
209        String fileAttribute = attributes.getValue(FILE_ATTR);
210        String urlAttribute = attributes.getValue(URL_ATTR);
211        String resourceAttribute = attributes.getValue(RESOURCE_ATTR);
212
213        if (!OptionHelper.isNullOrEmpty(fileAttribute)) {
214            this.attributeInUse = ec.subst(fileAttribute);
215            return filePathAsURL(attributeInUse);
216        }
217
218        if (!OptionHelper.isNullOrEmpty(urlAttribute)) {
219            this.attributeInUse = ec.subst(urlAttribute);
220            return attributeToURL(attributeInUse);
221        }
222
223        if (!OptionHelper.isNullOrEmpty(resourceAttribute)) {
224            this.attributeInUse = ec.subst(resourceAttribute);
225            return resourceAsURL(attributeInUse);
226        }
227        // given previous checkAttributes() check we cannot reach this line
228        throw new IllegalStateException("A URL stream should have been returned");
229
230    }
231
232    InputStream getInputStream(SaxEventInterpretationContext ec, Attributes attributes) {
233        URL inputURL = getInputURL(ec, attributes);
234        if (inputURL == null)
235            return null;
236
237        ConfigurationWatchListUtil.addToWatchList(context, inputURL);
238        return openURL(inputURL);
239    }
240
241    private void trimHeadAndTail(SaxEventRecorder recorder) {
242        // Let's remove the two <included> events before
243        // adding the events to the player.
244
245        // note saxEventList.size() changes over time as events are removed 
246        
247        List<SaxEvent> saxEventList = recorder.getSaxEventList();
248
249        if (saxEventList.size() == 0) {
250            return;
251        }
252
253        SaxEvent first = saxEventList.get(0);
254        if (first != null && first.qName.equalsIgnoreCase(INCLUDED_TAG)) {
255            saxEventList.remove(0);
256        }
257        
258        SaxEvent last = saxEventList.get(saxEventList.size() - 1);
259        if (last != null && last.qName.equalsIgnoreCase(INCLUDED_TAG)) {
260            saxEventList.remove(saxEventList.size() - 1);
261        }
262    }
263
264    private void parseAndRecord(InputStream inputSource, SaxEventRecorder recorder) throws JoranException {
265        recorder.setContext(context);
266        recorder.recordEvents(inputSource);
267    }
268
269    @Override
270    public void end(SaxEventInterpretationContext seic, String name) throws ActionException {
271        
272        if(inError)
273            return;
274        
275        Model m = seic.peekModel();
276
277        if (m != includeModel) {
278            addWarn("The object at the of the stack is not the model [" + includeModel.idString()
279                    + "] pushed earlier.");
280            addWarn("This is wholly unexpected.");
281        }
282
283        // do not pop nor add to parent if there is no parent
284        if (parentModel != null) {
285            parentModel.addSubModel(includeModel);
286            seic.popModel();
287        }
288    }
289}