Convert from NULL macro to nullptr.
[shibboleth/cpp-xmltooling.git] / xmltooling / util / ReloadableXMLFile.cpp
1 /*
2  *  Copyright 2001-2010 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 #if defined(XMLTOOLING_LOG4SHIB)
33 # include <log4shib/NDC.hh>
34 #elif defined(XMLTOOLING_LOG4CPP)
35 # include <log4cpp/NDC.hh>
36 #endif
37
38 #include <memory>
39 #include <fstream>
40 #include <sys/types.h>
41 #include <sys/stat.h>
42
43 #include <xercesc/framework/LocalFileInputSource.hpp>
44 #include <xercesc/framework/Wrapper4InputSource.hpp>
45 #include <xercesc/util/XMLUniDefs.hpp>
46
47 using namespace xmltooling::logging;
48 using namespace xmltooling;
49 using namespace xercesc;
50 using namespace std;
51
52 static const XMLCh id[] =               UNICODE_LITERAL_2(i,d);
53 static const XMLCh uri[] =              UNICODE_LITERAL_3(u,r,i);
54 static const XMLCh url[] =              UNICODE_LITERAL_3(u,r,l);
55 static const XMLCh path[] =             UNICODE_LITERAL_4(p,a,t,h);
56 static const XMLCh pathname[] =         UNICODE_LITERAL_8(p,a,t,h,n,a,m,e);
57 static const XMLCh file[] =             UNICODE_LITERAL_4(f,i,l,e);
58 static const XMLCh filename[] =         UNICODE_LITERAL_8(f,i,l,e,n,a,m,e);
59 static const XMLCh validate[] =         UNICODE_LITERAL_8(v,a,l,i,d,a,t,e);
60 static const XMLCh reloadChanges[] =    UNICODE_LITERAL_13(r,e,l,o,a,d,C,h,a,n,g,e,s);
61 static const XMLCh reloadInterval[] =   UNICODE_LITERAL_14(r,e,l,o,a,d,I,n,t,e,r,v,a,l);
62 static const XMLCh backingFilePath[] =  UNICODE_LITERAL_15(b,a,c,k,i,n,g,F,i,l,e,P,a,t,h);
63
64
65 ReloadableXMLFile::ReloadableXMLFile(const DOMElement* e, Category& log)
66     : m_root(e), m_local(true), m_validate(false), m_backupIndicator(true), m_filestamp(0), m_reloadInterval(0), m_lock(nullptr), m_log(log),
67         m_shutdown(false), m_reload_wait(nullptr), m_reload_thread(nullptr)
68 {
69 #ifdef _DEBUG
70     NDC ndc("ReloadableXMLFile");
71 #endif
72
73     // Establish source of data...
74     const XMLCh* source=e->getAttributeNS(nullptr,uri);
75     if (!source || !*source) {
76         source=e->getAttributeNS(nullptr,url);
77         if (!source || !*source) {
78             source=e->getAttributeNS(nullptr,path);
79             if (!source || !*source) {
80                 source=e->getAttributeNS(nullptr,pathname);
81                 if (!source || !*source) {
82                     source=e->getAttributeNS(nullptr,file);
83                     if (!source || !*source) {
84                         source=e->getAttributeNS(nullptr,filename);
85                     }
86                 }
87             }
88         }
89         else
90             m_local=false;
91     }
92     else
93         m_local=false;
94
95     if (source && *source) {
96         const XMLCh* flag=e->getAttributeNS(nullptr,validate);
97         m_validate=(XMLString::equals(flag,xmlconstants::XML_TRUE) || XMLString::equals(flag,xmlconstants::XML_ONE));
98
99         auto_ptr_char temp(source);
100         m_source=temp.get();
101
102         if (!m_local && !strstr(m_source.c_str(),"://")) {
103             log.warn("deprecated usage of uri/url attribute for a local resource, use path instead");
104             m_local=true;
105         }
106
107         if (m_local) {
108             XMLToolingConfig::getConfig().getPathResolver()->resolve(m_source, PathResolver::XMLTOOLING_CFG_FILE);
109
110             flag=e->getAttributeNS(nullptr,reloadChanges);
111             if (!XMLString::equals(flag,xmlconstants::XML_FALSE) && !XMLString::equals(flag,xmlconstants::XML_ZERO)) {
112 #ifdef WIN32
113                 struct _stat stat_buf;
114                 if (_stat(m_source.c_str(), &stat_buf) == 0)
115 #else
116                 struct stat stat_buf;
117                 if (stat(m_source.c_str(), &stat_buf) == 0)
118 #endif
119                     m_filestamp=stat_buf.st_mtime;
120                 else
121                     throw IOException("Unable to access local file ($1)", params(1,m_source.c_str()));
122                 m_lock=RWLock::create();
123             }
124             log.debug("using local resource (%s), will %smonitor for changes", m_source.c_str(), m_lock ? "" : "not ");
125         }
126         else {
127             log.debug("using remote resource (%s)", m_source.c_str());
128             source = e->getAttributeNS(nullptr,backingFilePath);
129             if (source && *source) {
130                 auto_ptr_char temp2(source);
131                 m_backing=temp2.get();
132                 XMLToolingConfig::getConfig().getPathResolver()->resolve(m_backing, PathResolver::XMLTOOLING_RUN_FILE);
133                 log.debug("backup remote resource to (%s)", m_backing.c_str());
134             }
135             source = e->getAttributeNS(nullptr,reloadInterval);
136             if (source && *source) {
137                 m_reloadInterval = XMLString::parseInt(source);
138                 if (m_reloadInterval > 0) {
139                     m_log.debug("will reload remote resource at most every %d seconds", m_reloadInterval);
140                     m_lock=RWLock::create();
141                 }
142             }
143             m_filestamp = time(nullptr);   // assume it gets loaded initially
144         }
145
146         if (m_lock) {
147             m_reload_wait = CondWait::create();
148             m_reload_thread = Thread::create(&reload_fn, this);
149         }
150     }
151     else {
152         log.debug("no resource uri/path/name supplied, will load inline configuration");
153     }
154
155     source = e->getAttributeNS(nullptr, id);
156     if (source && *source) {
157         auto_ptr_char tempid(source);
158         m_id = tempid.get();
159     }
160 }
161
162 ReloadableXMLFile::~ReloadableXMLFile()
163 {
164     shutdown();
165     delete m_lock;
166 }
167
168 void ReloadableXMLFile::shutdown()
169 {
170     if (m_reload_thread) {
171         // Shut down the reload thread and let it know.
172         m_shutdown = true;
173         m_reload_wait->signal();
174         m_reload_thread->join(nullptr);
175         delete m_reload_thread;
176         delete m_reload_wait;
177         m_reload_thread = nullptr;
178         m_reload_wait = nullptr;
179     }
180 }
181
182 void* ReloadableXMLFile::reload_fn(void* pv)
183 {
184     ReloadableXMLFile* r = reinterpret_cast<ReloadableXMLFile*>(pv);
185
186 #ifndef WIN32
187     // First, let's block all signals
188     Thread::mask_all_signals();
189 #endif
190
191     if (!r->m_id.empty()) {
192         string threadid("[");
193         threadid += r->m_id + ']';
194         logging::NDC::push(threadid);
195     }
196
197 #ifdef _DEBUG
198     NDC ndc("reload");
199 #endif
200
201     auto_ptr<Mutex> mutex(Mutex::create());
202     mutex->lock();
203
204     if (r->m_local)
205         r->m_log.info("reload thread started...running when signaled");
206     else
207         r->m_log.info("reload thread started...running every %d seconds", r->m_reloadInterval);
208
209     while (!r->m_shutdown) {
210         if (r->m_local)
211             r->m_reload_wait->wait(mutex.get());
212         else
213             r->m_reload_wait->timedwait(mutex.get(), r->m_reloadInterval);
214         if (r->m_shutdown)
215             break;
216
217         try {
218             r->m_log.info("reloading %s resource...", r->m_local ? "local" : "remote");
219             pair<bool,DOMElement*> ret = r->background_load();
220             if (ret.first)
221                 ret.second->getOwnerDocument()->release();
222         }
223         catch (long& ex) {
224             if (ex == HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED) {
225                 r->m_log.info("remote resource (%s) unchanged from cached version", r->m_source.c_str());
226             }
227             else {
228                 // Shouldn't happen, we should only get codes intended to be gracefully handled.
229                 r->m_log.crit("maintaining existing configuration, remote resource fetch returned atypical status code (%d)", ex);
230             }
231         }
232         catch (exception& ex) {
233             r->m_log.crit("maintaining existing configuration, error reloading resource (%s): %s", r->m_source.c_str(), ex.what());
234         }
235     }
236
237     r->m_log.info("reload thread finished");
238
239     mutex->unlock();
240
241     if (!r->m_id.empty()) {
242         logging::NDC::pop();
243     }
244
245     return nullptr;
246 }
247
248 Lockable* ReloadableXMLFile::lock()
249 {
250     if (!m_lock)
251         return this;
252
253     m_lock->rdlock();
254
255     if (m_local) {
256     // Check if we need to refresh.
257 #ifdef WIN32
258         struct _stat stat_buf;
259         if (_stat(m_source.c_str(), &stat_buf) != 0)
260             return this;
261 #else
262         struct stat stat_buf;
263         if (stat(m_source.c_str(), &stat_buf) != 0)
264             return this;
265 #endif
266         if (m_filestamp >= stat_buf.st_mtime)
267             return this;
268
269         // Elevate lock and recheck.
270         m_log.debug("timestamp of local resource changed, elevating to a write lock");
271         m_lock->unlock();
272         m_lock->wrlock();
273         if (m_filestamp >= stat_buf.st_mtime) {
274             // Somebody else handled it, just downgrade.
275             m_log.debug("update of local resource handled by another thread, downgrading lock");
276             m_lock->unlock();
277             m_lock->rdlock();
278             return this;
279         }
280
281         // Update the timestamp regardless.
282         m_filestamp = stat_buf.st_mtime;
283         m_log.info("change detected, signaling reload thread...");
284         m_reload_wait->signal();
285     }
286
287     return this;
288 }
289
290 void ReloadableXMLFile::unlock()
291 {
292     if (m_lock)
293         m_lock->unlock();
294 }
295
296 pair<bool,DOMElement*> ReloadableXMLFile::load(bool backup)
297 {
298 #ifdef _DEBUG
299     NDC ndc("load");
300 #endif
301
302     try {
303         if (m_source.empty()) {
304             // Data comes from the DOM we were handed.
305             m_log.debug("loading inline configuration...");
306             return make_pair(false, XMLHelper::getFirstChildElement(m_root));
307         }
308         else {
309             // Data comes from a file we have to parse.
310             if (backup)
311                 m_log.warn("using local backup of remote resource");
312             else
313                 m_log.debug("loading configuration from external resource...");
314
315             DOMDocument* doc=nullptr;
316             if (m_local || backup) {
317                 auto_ptr_XMLCh widenit(backup ? m_backing.c_str() : m_source.c_str());
318                 // Use library-wide lock for now, nothing else is using it anyway.
319                 Locker locker(backup ? getBackupLock() : nullptr);
320                 LocalFileInputSource src(widenit.get());
321                 Wrapper4InputSource dsrc(&src, false);
322                 if (m_validate)
323                     doc=XMLToolingConfig::getConfig().getValidatingParser().parse(dsrc);
324                 else
325                     doc=XMLToolingConfig::getConfig().getParser().parse(dsrc);
326             }
327             else {
328                 URLInputSource src(m_root, nullptr, &m_cacheTag);
329                 Wrapper4InputSource dsrc(&src, false);
330                 if (m_validate)
331                     doc=XMLToolingConfig::getConfig().getValidatingParser().parse(dsrc);
332                 else
333                     doc=XMLToolingConfig::getConfig().getParser().parse(dsrc);
334
335                 // Check for a response code signal.
336                 if (XMLHelper::isNodeNamed(doc->getDocumentElement(), xmlconstants::XMLTOOLING_NS, URLInputSource::utf16StatusCodeElementName)) {
337                     int responseCode = XMLString::parseInt(doc->getDocumentElement()->getFirstChild()->getNodeValue());
338                     doc->release();
339                     if (responseCode == HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED) {
340                         throw (long)responseCode; // toss out as a "known" case to handle gracefully
341                     }
342                     else {
343                         m_log.warn("remote resource fetch returned atypical status code (%d)", responseCode);
344                         throw IOException("remote resource fetch failed, check log for status code of response");
345                     }
346                 }
347             }
348
349             m_log.infoStream() << "loaded XML resource (" << (backup ? m_backing : m_source) << ")" << logging::eol;
350
351             if (!backup && !m_backing.empty()) {
352                 // If the indicator is true, we're responsible for the backup.
353                 if (m_backupIndicator) {
354                     m_log.debug("backing up remote resource to (%s)", m_backing.c_str());
355                     try {
356                         Locker locker(getBackupLock());
357                         ofstream backer(m_backing.c_str());
358                         backer << *doc;
359                     }
360                     catch (exception& ex) {
361                         m_log.crit("exception while backing up resource: %s", ex.what());
362                     }
363                 }
364                 else {
365                     // If the indicator was false, set true to signal that a backup is needed.
366                     // The caller will presumably flip it back to false once that's done.
367                     m_backupIndicator = true;
368                 }
369             }
370
371             return make_pair(true, doc->getDocumentElement());
372         }
373     }
374     catch (XMLException& e) {
375         auto_ptr_char msg(e.getMessage());
376         m_log.errorStream() << "Xerces error while loading resource (" << (backup ? m_backing : m_source) << "): "
377             << msg.get() << logging::eol;
378         if (!backup && !m_backing.empty())
379             return load(true);
380         throw XMLParserException(msg.get());
381     }
382     catch (exception& e) {
383         m_log.errorStream() << "error while loading resource ("
384             << (m_source.empty() ? "inline" : (backup ? m_backing : m_source)) << "): " << e.what() << logging::eol;
385         if (!backup && !m_backing.empty())
386             return load(true);
387         throw;
388     }
389 }
390
391 pair<bool,DOMElement*> ReloadableXMLFile::load()
392 {
393     return load(false);
394 }
395
396 pair<bool,DOMElement*> ReloadableXMLFile::background_load()
397 {
398     // If this method isn't overridden, we acquire a write lock
399     // and just call the old override.
400     if (m_lock)
401         m_lock->wrlock();
402     SharedLock locker(m_lock, false);
403     return load();
404 }
405
406 Lockable* ReloadableXMLFile::getBackupLock()
407 {
408     return &XMLToolingConfig::getConfig();
409 }