https://issues.shibboleth.net/jira/browse/CPPXT-52
[shibboleth/cpp-xmltooling.git] / xmltooling / util / ReloadableXMLFile.cpp
1 /*
2  *  Copyright 2001-2009 Internet2
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /**
18  * @file ReloadableXMLFile.cpp
19  *
20  * Base class for file-based XML configuration.
21  */
22
23 #include "internal.h"
24 #include "io/HTTPResponse.h"
25 #include "util/NDC.h"
26 #include "util/PathResolver.h"
27 #include "util/ReloadableXMLFile.h"
28 #include "util/Threads.h"
29 #include "util/XMLConstants.h"
30 #include "util/XMLHelper.h"
31
32 #include <fstream>
33 #include <sys/types.h>
34 #include <sys/stat.h>
35
36 #include <xercesc/framework/LocalFileInputSource.hpp>
37 #include <xercesc/framework/Wrapper4InputSource.hpp>
38 #include <xercesc/util/XMLUniDefs.hpp>
39
40 using namespace xmltooling::logging;
41 using namespace xmltooling;
42 using namespace xercesc;
43 using namespace std;
44
45 static const XMLCh uri[] =              UNICODE_LITERAL_3(u,r,i);
46 static const XMLCh url[] =              UNICODE_LITERAL_3(u,r,l);
47 static const XMLCh path[] =             UNICODE_LITERAL_4(p,a,t,h);
48 static const XMLCh pathname[] =         UNICODE_LITERAL_8(p,a,t,h,n,a,m,e);
49 static const XMLCh file[] =             UNICODE_LITERAL_4(f,i,l,e);
50 static const XMLCh filename[] =         UNICODE_LITERAL_8(f,i,l,e,n,a,m,e);
51 static const XMLCh validate[] =         UNICODE_LITERAL_8(v,a,l,i,d,a,t,e);
52 static const XMLCh reloadChanges[] =    UNICODE_LITERAL_13(r,e,l,o,a,d,C,h,a,n,g,e,s);
53 static const XMLCh reloadInterval[] =   UNICODE_LITERAL_14(r,e,l,o,a,d,I,n,t,e,r,v,a,l);
54 static const XMLCh backingFilePath[] =  UNICODE_LITERAL_15(b,a,c,k,i,n,g,F,i,l,e,P,a,t,h);
55
56
57 ReloadableXMLFile::~ReloadableXMLFile()
58 {
59     delete m_lock;
60 }
61
62 ReloadableXMLFile::ReloadableXMLFile(const DOMElement* e, Category& log)
63     : m_root(e), m_local(true), m_validate(false), m_filestamp(0), m_reloadInterval(0), m_lock(NULL), m_log(log)
64 {
65 #ifdef _DEBUG
66     NDC ndc("ReloadableXMLFile");
67 #endif
68
69     // Establish source of data...
70     const XMLCh* source=e->getAttributeNS(NULL,uri);
71     if (!source || !*source) {
72         source=e->getAttributeNS(NULL,url);
73         if (!source || !*source) {
74             source=e->getAttributeNS(NULL,path);
75             if (!source || !*source) {
76                 source=e->getAttributeNS(NULL,pathname);
77                 if (!source || !*source) {
78                     source=e->getAttributeNS(NULL,file);
79                     if (!source || !*source) {
80                         source=e->getAttributeNS(NULL,filename);
81                     }
82                 }
83             }
84         }
85         else
86             m_local=false;
87     }
88     else
89         m_local=false;
90
91     if (source && *source) {
92         const XMLCh* flag=e->getAttributeNS(NULL,validate);
93         m_validate=(XMLString::equals(flag,xmlconstants::XML_TRUE) || XMLString::equals(flag,xmlconstants::XML_ONE));
94
95         auto_ptr_char temp(source);
96         m_source=temp.get();
97
98         if (!m_local && !strstr(m_source.c_str(),"://")) {
99             log.warn("deprecated usage of uri/url attribute for a local resource, use path instead");
100             m_local=true;
101         }
102
103         if (m_local) {
104             XMLToolingConfig::getConfig().getPathResolver()->resolve(m_source, PathResolver::XMLTOOLING_CFG_FILE);
105
106             flag=e->getAttributeNS(NULL,reloadChanges);
107             if (!XMLString::equals(flag,xmlconstants::XML_FALSE) && !XMLString::equals(flag,xmlconstants::XML_ZERO)) {
108 #ifdef WIN32
109                 struct _stat stat_buf;
110                 if (_stat(m_source.c_str(), &stat_buf) == 0)
111 #else
112                 struct stat stat_buf;
113                 if (stat(m_source.c_str(), &stat_buf) == 0)
114 #endif
115                     m_filestamp=stat_buf.st_mtime;
116                 else
117                     throw IOException("Unable to access local file ($1)", params(1,m_source.c_str()));
118                 m_lock=RWLock::create();
119             }
120             log.debug("using local resource (%s), will %smonitor for changes", m_source.c_str(), m_lock ? "" : "not ");
121         }
122         else {
123             log.debug("using remote resource (%s)", m_source.c_str());
124             source = e->getAttributeNS(NULL,backingFilePath);
125             if (source && *source) {
126                 auto_ptr_char temp2(source);
127                 m_backing=temp2.get();
128                 XMLToolingConfig::getConfig().getPathResolver()->resolve(m_backing, PathResolver::XMLTOOLING_RUN_FILE);
129                 log.debug("backup remote resource with (%s)", m_backing.c_str());
130             }
131             source = e->getAttributeNS(NULL,reloadInterval);
132             if (source && *source) {
133                 m_reloadInterval = XMLString::parseInt(source);
134                 if (m_reloadInterval > 0) {
135                     m_log.debug("will reload remote resource at most every %d seconds", m_reloadInterval);
136                     m_lock=RWLock::create();
137                 }
138             }
139             m_filestamp = time(NULL);   // assume it gets loaded initially
140         }
141     }
142     else {
143         log.debug("no resource uri/path/name supplied, will load inline configuration");
144     }
145 }
146
147 pair<bool,DOMElement*> ReloadableXMLFile::load(bool backup)
148 {
149 #ifdef _DEBUG
150     NDC ndc("init");
151 #endif
152
153     try {
154         if (m_source.empty()) {
155             // Data comes from the DOM we were handed.
156             m_log.debug("loading inline configuration...");
157             return make_pair(false,XMLHelper::getFirstChildElement(m_root));
158         }
159         else {
160             // Data comes from a file we have to parse.
161             if (backup)
162                 m_log.warn("using local backup of remote resource");
163             else
164                 m_log.debug("loading configuration from external resource...");
165
166             DOMDocument* doc=NULL;
167             if (m_local || backup) {
168                 auto_ptr_XMLCh widenit(backup ? m_backing.c_str() : m_source.c_str());
169                 LocalFileInputSource src(widenit.get());
170                 Wrapper4InputSource dsrc(&src,false);
171                 if (m_validate)
172                     doc=XMLToolingConfig::getConfig().getValidatingParser().parse(dsrc);
173                 else
174                     doc=XMLToolingConfig::getConfig().getParser().parse(dsrc);
175             }
176             else {
177                 URLInputSource src(m_root, NULL, &m_cacheTag);
178                 Wrapper4InputSource dsrc(&src,false);
179                 if (m_validate)
180                     doc=XMLToolingConfig::getConfig().getValidatingParser().parse(dsrc);
181                 else
182                     doc=XMLToolingConfig::getConfig().getParser().parse(dsrc);
183
184                 // Check for a response code signal.
185                 if (XMLHelper::isNodeNamed(doc->getDocumentElement(), xmlconstants::XMLTOOLING_NS, URLInputSource::utf16StatusCodeElementName)) {
186                     int responseCode = XMLString::parseInt(doc->getDocumentElement()->getFirstChild()->getNodeValue());
187                     doc->release();
188                     if (responseCode == HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED) {
189                         throw responseCode; // toss out as a "known" case to handle gracefully
190                     }
191                     else {
192                         m_log.warn("remote resource fetch returned atypical status code (%d)", responseCode);
193                         throw IOException("remote resource fetch failed, check log for status code of response");
194                     }
195                 }
196             }
197
198             m_log.infoStream() << "loaded XML resource (" << (backup ? m_backing : m_source) << ")" << logging::eol;
199
200             if (!backup && !m_backing.empty()) {
201                 m_log.debug("backing up remote resource to (%s)", m_backing.c_str());
202                 try {
203                     ofstream backer(m_backing.c_str());
204                     backer << *doc;
205                 }
206                 catch (exception& ex) {
207                     m_log.crit("exception while backing up resource: %s", ex.what());
208                 }
209             }
210
211             return make_pair(true,doc->getDocumentElement());
212         }
213     }
214     catch (XMLException& e) {
215         auto_ptr_char msg(e.getMessage());
216         m_log.errorStream() << "Xerces error while loading resource (" << (backup ? m_backing : m_source) << "): "
217             << msg.get() << logging::eol;
218         if (!backup && !m_backing.empty())
219             return load(true);
220         throw XMLParserException(msg.get());
221     }
222     catch (exception& e) {
223         m_log.errorStream() << "error while loading resource ("
224             << (m_source.empty() ? "inline" : (backup ? m_backing : m_source)) << "): " << e.what() << logging::eol;
225         if (!backup && !m_backing.empty())
226             return load(true);
227         throw;
228     }
229 }
230
231 Lockable* ReloadableXMLFile::lock()
232 {
233     if (!m_lock)
234         return this;
235
236     m_lock->rdlock();
237
238     // Check if we need to refresh.
239     if (m_local) {
240 #ifdef WIN32
241         struct _stat stat_buf;
242         if (_stat(m_source.c_str(), &stat_buf) != 0)
243             return this;
244 #else
245         struct stat stat_buf;
246         if (stat(m_source.c_str(), &stat_buf) != 0)
247             return this;
248 #endif
249         if (m_filestamp>=stat_buf.st_mtime)
250             return this;
251
252         // Elevate lock and recheck.
253         m_log.debug("timestamp of local resource changed, elevating to a write lock");
254         m_lock->unlock();
255         m_lock->wrlock();
256         if (m_filestamp>=stat_buf.st_mtime) {
257             // Somebody else handled it, just downgrade.
258             m_log.debug("update of local resource handled by another thread, downgrading lock");
259             m_lock->unlock();
260             m_lock->rdlock();
261             return this;
262         }
263
264         // Update the timestamp regardless. No point in repeatedly trying.
265         m_filestamp=stat_buf.st_mtime;
266         m_log.info("change detected, reloading local resource...");
267     }
268     else {
269         time_t now = time(NULL);
270
271         // Time to reload? If we have no data, filestamp is zero
272         // and there's no way current time is less than the interval.
273         if (now - m_filestamp < m_reloadInterval)
274             return this;
275
276         // Elevate lock and recheck.
277         m_log.debug("reload interval for remote resource elapsed, elevating to a write lock");
278         m_lock->unlock();
279         m_lock->wrlock();
280         if (now - m_filestamp < m_reloadInterval) {
281             // Somebody else handled it, just downgrade.
282             m_log.debug("update of remote resource handled by another thread, downgrading lock");
283             m_lock->unlock();
284             m_lock->rdlock();
285             return this;
286         }
287
288         m_filestamp = now;
289         m_log.info("reloading remote resource...");
290     }
291
292     // Do this once...
293     try {
294         // At this point we're holding the write lock, so make sure we pop it.
295         SharedLock lockwrap(m_lock,false);
296         pair<bool,DOMElement*> ret=load();
297         if (ret.first)
298             ret.second->getOwnerDocument()->release();
299     }
300     catch (int& ex) {
301         if (ex == HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED) {
302             m_log.info("remote resource (%s) unchanged from cached version", m_source.c_str());
303         }
304         else {
305             // Shouldn't happen, we should only get codes intended to be gracefully handled.
306             m_log.crit("maintaining existing configuration, remote resource fetch returned atypical status code (%d)", ex);
307         }
308     }
309     catch (exception& ex) {
310         m_log.crit("maintaining existing configuration, error reloading resource (%s): %s", m_source.c_str(), ex.what());
311     }
312
313     // If we made it here, the swap may or may not have worked, but we need to relock.
314     m_log.debug("attempt to update resource complete, relocking");
315     m_lock->rdlock();
316     return this;
317 }
318
319 void ReloadableXMLFile::unlock()
320 {
321     if (m_lock)
322         m_lock->unlock();
323 }
324
325 pair<bool,DOMElement*> ReloadableXMLFile::load()
326 {
327     return load(false);
328 }