001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2024, 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 */
014
015package ch.qos.logback.core.model.processor;
016
017import ch.qos.logback.core.Context;
018import ch.qos.logback.core.joran.GenericXMLConfigurator;
019import ch.qos.logback.core.joran.event.SaxEvent;
020import ch.qos.logback.core.joran.event.SaxEventRecorder;
021import ch.qos.logback.core.joran.spi.JoranException;
022import ch.qos.logback.core.joran.util.ConfigurationWatchListUtil;
023import ch.qos.logback.core.model.IncludeModel;
024import ch.qos.logback.core.model.Model;
025import ch.qos.logback.core.spi.ErrorCodes;
026import ch.qos.logback.core.util.Loader;
027import ch.qos.logback.core.util.OptionHelper;
028
029import java.io.File;
030import java.io.IOException;
031import java.io.InputStream;
032import java.net.MalformedURLException;
033import java.net.URI;
034import java.net.URL;
035import java.util.List;
036import java.util.function.Supplier;
037
038import static ch.qos.logback.core.joran.JoranConstants.CONFIGURATION_TAG;
039import static ch.qos.logback.core.joran.JoranConstants.INCLUDED_TAG;
040
041/**
042 * @since 1.5.5
043 */
044public class IncludeModelHandler extends ModelHandlerBase {
045    boolean inError = false;
046    private String attributeInUse;
047    private boolean optional;
048
049    public IncludeModelHandler(Context context) {
050        super(context);
051    }
052
053    static public IncludeModelHandler makeInstance(Context context, ModelInterpretationContext mic) {
054        return new IncludeModelHandler(context);
055    }
056
057    @Override
058    protected Class<IncludeModel> getSupportedModelClass() {
059        return IncludeModel.class;
060    }
061
062    @Override
063    public void handle(ModelInterpretationContext mic, Model model) throws ModelHandlerException {
064        IncludeModel includeModel = (IncludeModel) model;
065
066        this.optional = OptionHelper.toBoolean(includeModel.getOptional(), false);
067
068        if (!checkAttributes(includeModel)) {
069            inError = true;
070            return;
071        }
072
073        InputStream in = getInputStream(mic, includeModel);
074        if(in == null) {
075            inError = true;
076            return;
077        }
078
079        SaxEventRecorder recorder = null;
080
081        try {
082            recorder = populateSaxEventRecorder(in);
083
084            List<SaxEvent> saxEvents = recorder.getSaxEventList();
085            if (saxEvents.isEmpty()) {
086                addWarn("Empty sax event list");
087                return;
088            }
089
090            //trimHeadAndTail(saxEvents);
091
092            Supplier<? extends GenericXMLConfigurator> jcSupplier = mic.getConfiguratorSupplier();
093            if (jcSupplier == null) {
094                addError("null configurator supplier. Abandoning inclusion of [" + attributeInUse + "]");
095                inError = true;
096                return;
097            }
098
099            GenericXMLConfigurator genericXMLConfigurator = jcSupplier.get();
100            genericXMLConfigurator.getRuleStore().addPathPathMapping(INCLUDED_TAG, CONFIGURATION_TAG);
101
102            Model modelFromIncludedFile = genericXMLConfigurator.buildModelFromSaxEventList(recorder.getSaxEventList());
103            if (modelFromIncludedFile == null) {
104                addError(ErrorCodes.EMPTY_MODEL_STACK);
105                return;
106            }
107
108            includeModel.getSubModels().addAll(modelFromIncludedFile.getSubModels());
109
110        } catch (JoranException e) {
111            inError = true;
112            addError("Error processing XML data in [" + attributeInUse + "]", e);
113        }
114    }
115
116    public SaxEventRecorder populateSaxEventRecorder(final InputStream inputStream) throws JoranException {
117        SaxEventRecorder recorder = new SaxEventRecorder(context);
118        recorder.recordEvents(inputStream);
119        return recorder;
120    }
121
122    private void trimHeadAndTail( List<SaxEvent> saxEventList) {
123        // Let's remove the two <included> events before
124        // adding the events to the player.
125
126        // note saxEventList.size() changes over time as events are removed
127
128        if (saxEventList.size() == 0) {
129            return;
130        }
131
132        SaxEvent first = saxEventList.get(0);
133        if (first != null && first.qName.equalsIgnoreCase(INCLUDED_TAG)) {
134            saxEventList.remove(0);
135        }
136
137        SaxEvent last = saxEventList.get(saxEventList.size() - 1);
138        if (last != null && last.qName.equalsIgnoreCase(INCLUDED_TAG)) {
139            saxEventList.remove(saxEventList.size() - 1);
140        }
141    }
142
143    InputStream getInputStream(ModelInterpretationContext mic, IncludeModel includeModel) {
144        URL inputURL = getInputURL(mic, includeModel);
145        if (inputURL == null)
146            return null;
147
148        ConfigurationWatchListUtil.addToWatchList(context, inputURL);
149        return openURL(inputURL);
150    }
151
152    InputStream openURL(URL url) {
153        try {
154            return url.openStream();
155        } catch (IOException e) {
156            optionalWarning("Failed to open [" + url.toString() + "]");
157            return null;
158        }
159    }
160
161    private boolean checkAttributes(IncludeModel includeModel) {
162        String fileAttribute = includeModel.getFile();
163        String urlAttribute = includeModel.getUrl();
164        String resourceAttribute = includeModel.getResource();
165
166        int count = 0;
167
168        if (!OptionHelper.isNullOrEmptyOrAllSpaces(fileAttribute)) {
169            count++;
170        }
171        if (!OptionHelper.isNullOrEmptyOrAllSpaces(urlAttribute)) {
172            count++;
173        }
174        if (!OptionHelper.isNullOrEmptyOrAllSpaces(resourceAttribute)) {
175            count++;
176        }
177
178        if (count == 0) {
179            addError("One of \"path\", \"resource\" or \"url\" attributes must be set.");
180            return false;
181        } else if (count > 1) {
182            addError("Only one of \"file\", \"url\" or \"resource\" attributes should be set.");
183            return false;
184        } else if (count == 1) {
185            return true;
186        }
187        throw new IllegalStateException("Count value [" + count + "] is not expected");
188    }
189
190    URL getInputURL(ModelInterpretationContext mic, IncludeModel includeModel) {
191        String fileAttribute = includeModel.getFile();
192        String urlAttribute = includeModel.getUrl();
193        String resourceAttribute = includeModel.getResource();
194
195        if (!OptionHelper.isNullOrEmptyOrAllSpaces(fileAttribute)) {
196            this.attributeInUse = mic.subst(fileAttribute);
197            return filePathAsURL(attributeInUse);
198        }
199
200        if (!OptionHelper.isNullOrEmptyOrAllSpaces(urlAttribute)) {
201            this.attributeInUse = mic.subst(urlAttribute);
202            return attributeToURL(attributeInUse);
203        }
204
205        if (!OptionHelper.isNullOrEmptyOrAllSpaces(resourceAttribute)) {
206            this.attributeInUse = mic.subst(resourceAttribute);
207            return resourceAsURL(attributeInUse);
208        }
209        // given preceding checkAttributes() check we cannot reach this line
210        throw new IllegalStateException("A URL stream should have been returned at this stage");
211
212    }
213
214    URL filePathAsURL(String path) {
215        URI uri = new File(path).toURI();
216        try {
217            return uri.toURL();
218        } catch (MalformedURLException e) {
219            // impossible to get here
220            e.printStackTrace();
221            return null;
222        }
223    }
224
225    URL attributeToURL(String urlAttribute) {
226        try {
227            return new URL(urlAttribute);
228        } catch (MalformedURLException mue) {
229            String errMsg = "URL [" + urlAttribute + "] is not well formed.";
230            addError(errMsg, mue);
231            return null;
232        }
233    }
234
235    URL resourceAsURL(String resourceAttribute) {
236        URL url = Loader.getResourceBySelfClassLoader(resourceAttribute);
237        if (url == null) {
238            optionalWarning("Could not find resource corresponding to [" + resourceAttribute + "]");
239            return null;
240        } else
241            return url;
242    }
243
244    private void optionalWarning(String msg) {
245        if (!optional) {
246            addWarn(msg);
247        }
248    }
249}