Mobrovac has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/247274

Change subject: Update to service-template-node v0.2.2
......................................................................

Update to service-template-node v0.2.2

- Logging: log only whitelisted headers for privacy reasons
- Routes: fully-asynchronous route loading
- Bump dependencies' versions

Change-Id: Iffd68d0a2dfc806d471e64535027ee4004a7d4af
---
M .gitignore
M .jshintignore
M .jshintrc
A .travis.yml
A LICENSE
M app.js
M config.dev.yaml
A config.prod.yaml
A dist/init-scripts/systemd.erb
A dist/init-scripts/sysvinit.erb
A dist/init-scripts/upstart.erb
M lib/util.js
M package.json
M routes/citoid.js
A scripts/gen-init-scripts.rb
M static/index.html
M test/features/app/spec.js
M test/utils/assert.js
M test/utils/logStream.js
M test/utils/server.js
20 files changed, 995 insertions(+), 400 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/services/citoid 
refs/changes/74/247274/1

diff --git a/.gitignore b/.gitignore
index 5dafb2d..850a956 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
 Dockerfile
-*~
-.DS_Store
-*.log
-node_modules
+.idea/
 coverage
+config.yaml
+node_modules
+npm-debug.log
diff --git a/.jshintignore b/.jshintignore
index 1c69eee..a4c38eb 100644
--- a/.jshintignore
+++ b/.jshintignore
@@ -1,3 +1,4 @@
 coverage
 node_modules
 test
+static
diff --git a/.jshintrc b/.jshintrc
index f9c3280..258037d 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -1,38 +1,36 @@
 {
+    "predef": [
+        "Map",
+        "Set"
+    ],
 
-       "predef": [
-               "Map",
-               "Set"
-       ],
+    // Enforcing
+    "bitwise": true,
+    "eqeqeq": true,
+    "freeze": true,
+    "noarg": true,
+    "nonew": true,
+    "undef": true,
 
-       // Enforcing
-       "bitwise": true,
-       "eqeqeq": true,
-       "freeze": true,
-       "noarg": true,
-       "nonew": true,
-       "undef": true,
+    "curly": true,
+    "newcap": true,
+    "noempty": true,
+    "immed": true,
 
-       "curly": true,
-       "newcap": true,
-       "noempty": true,
-       "immed": true,
+    // Relaxing
+    "laxbreak": true,
+    "sub": true,
+    "boss": true,
+    "eqnull": true,
+    "multistr": true,
+    "loopfunc": true,
 
-       // Relaxing
-       "laxbreak": true,
-       "sub": true,
-       "boss": true,
-       "eqnull": true,
-       "multistr": true,
-       "loopfunc": true,
+    // Invalid options (?)
+    "regexp": false,
+    "trailing": true,
+    "smarttabs": true,
+    "nomen": false,
 
-       // Invalid options (?)
-       "regexp": false,
-       "trailing": true,
-       "smarttabs": true,
-       "nomen": false,
-
-       "node": true,
-       "esnext": true
-
+    "node": true,
+    "esnext": true
 }
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..47f2d25
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,7 @@
+language: node_js
+node_js:
+    - "0.10"
+    - "0.11"
+    - "0.12"
+    - "iojs"
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e06d208
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
diff --git a/app.js b/app.js
index b8eb129..89baadd 100644
--- a/app.js
+++ b/app.js
@@ -8,7 +8,6 @@
 var bodyParser = require('body-parser');
 var fs = BBPromise.promisifyAll(require('fs'));
 var sUtil = require('./lib/util');
-var CitoidService = require('./lib/CitoidService');
 var packageInfo = require('./package.json');
 var yaml = require('js-yaml');
 
@@ -20,104 +19,105 @@
  */
 function initApp(options) {
 
-       // the main application object
-       var app = express();
+    // the main application object
+    var app = express();
 
-       // get the options and make them available in the app
-       app.logger = options.logger;    // the logging device
-       app.metrics = options.metrics;  // the metrics
-       app.conf = options.config;      // this app's config options
-       app.info = packageInfo;         // this app's package info
+    // get the options and make them available in the app
+    app.logger = options.logger;    // the logging device
+    app.metrics = options.metrics;  // the metrics
+    app.conf = options.config;      // this app's config options
+    app.info = packageInfo;         // this app's package info
 
-       // ensure some sane defaults
-       if(!app.conf.port) { app.conf.port = 1970; }
-       if(!app.conf.interface) { app.conf.interface = '0.0.0.0'; }
-       if(!app.conf.compression_level) { app.conf.compression_level = 3; }
-       if(app.conf.cors === undefined) { app.conf.cors = '*'; }
-       if(!app.conf.csp) {
-               app.conf.csp =
-                       "default-src 'self'; object-src 'none'; media-src *; 
img-src *; style-src *; frame-ancestors 'self'";
-       }
-
-       // set outgoing proxy
-       if(app.conf.proxy) {
-               process.env.HTTP_PROXY = app.conf.proxy;
-               if(!app.conf.zoteroUseProxy) {
-                       // don't use proxy for accessing Zotero unless 
specified in settings
-                       process.env.NO_PROXY = app.conf.zoteroInterface;
-               }
-       }
-
-       // ensure the User-Agent header is set
-       if(!app.conf.userAgent) { app.conf.userAgent = 'Citoid/' + 
app.info.version; }
-
-       // set up the spec
-       if(!app.conf.spec) {
-               app.conf.spec = __dirname + '/spec.yaml';
-       }
-       if(app.conf.spec.constructor !== Object) {
-               try {
-                       app.conf.spec = 
yaml.safeLoad(fs.readFileSync(app.conf.spec));
-               } catch(e) {
-                       app.logger.log('warn/spec', 'Could not load the spec: ' 
+ e);
-                       app.conf.spec = {};
-               }
-       }
-       if(!app.conf.spec.swagger) {
-               app.conf.spec.swagger = '2.0';
-       }
-       if(!app.conf.spec.info) {
-               app.conf.spec.info = {
-                       version: app.info.version,
-                       title: app.info.name,
-                       description: app.info.description
-               };
-       }
-       app.conf.spec.info.version = app.info.version;
-       if(!app.conf.spec.paths) {
-               app.conf.spec.paths = {};
-       }
-
-       // set the CORS and CSP headers
-       app.all('*', function(req, res, next) {
-               if(app.conf.cors !== false) {
-                       res.header("Access-Control-Allow-Origin", 
app.conf.cors);
-                       res.header("Access-Control-Allow-Headers", 
"X-Requested-With, Content-Type");
-               }
-               res.header('X-XSS-Protection', '1; mode=block');
-               res.header('X-Content-Type-Options', 'nosniff');
-               res.header('X-Frame-Options', 'SAMEORIGIN');
-               res.header('Content-Security-Policy', app.conf.csp);
-               res.header('X-Content-Security-Policy', app.conf.csp);
-               res.header('X-WebKit-CSP', app.conf.csp);
-               next();
-       });
-       // disable the X-Powered-By header
-       app.set('x-powered-by', false);
-       // disable the ETag header - users should provide them!
-       app.set('etag', false);
-       // enable compression
-       app.use(compression({level: app.conf.compression_level}));
-       // use the JSON body parser
-       app.use(bodyParser.json());
-       // use the application/x-www-form-urlencoded parser
-       app.use(bodyParser.urlencoded({extended: true}));
-       // set allowed export formats and expected response type
-       app.nativeFormats = {
-               'mediawiki':'application/json',
-               'zotero':'application/json',
-               'mwDeprecated':'application/json'
-       };
-       app.zoteroFormats = {
-               'bibtex':'application/x-bibtex'
-       };
-       app.formats = Object.assign({}, app.nativeFormats, app.zoteroFormats);
+    // ensure some sane defaults
+    if(!app.conf.port) { app.conf.port = 1970; }
+    if(!app.conf.interface) { app.conf.interface = '0.0.0.0'; }
+    if(!app.conf.compression_level) { app.conf.compression_level = 3; }
+    if(app.conf.cors === undefined) { app.conf.cors = '*'; }
+    if(!app.conf.csp) {
+        app.conf.csp =
+            "default-src 'self'; object-src 'none'; media-src *; img-src *; 
style-src *; frame-ancestors 'self'";
+    }
+    if(!app.conf.userAgent) { app.conf.userAgent = 'Citoid/' + 
app.info.version; }
 
 
-       // init the Citoid service object
-       app.citoid = new CitoidService(app);
+    // set outgoing proxy
+    if(app.conf.proxy) {
+        process.env.HTTP_PROXY = app.conf.proxy;
+        if(!app.conf.zoteroUseProxy) {
+            // don't use proxy for accessing Zotero unless specified in 
settings
+            process.env.NO_PROXY = app.conf.zoteroInterface;
+        }
+    }
 
-       return BBPromise.resolve(app);
+    // set up header whitelisting for logging
+    if(!app.conf.log_header_whitelist) {
+        app.conf.log_header_whitelist = [
+                'cache-control', 'content-type', 'content-length', 'if-match',
+                'user-agent', 'x-request-id'
+        ];
+    }
+    app.conf.log_header_whitelist = new RegExp('^(?:' + 
app.conf.log_header_whitelist.map(function(item) {
+        return item.trim();
+    }).join('|') + ')$', 'i');
+
+    // set up the spec
+    if(!app.conf.spec) {
+        app.conf.spec = __dirname + '/spec.yaml';
+    }
+    if(app.conf.spec.constructor !== Object) {
+        try {
+            app.conf.spec = yaml.safeLoad(fs.readFileSync(app.conf.spec));
+        } catch(e) {
+            app.logger.log('warn/spec', 'Could not load the spec: ' + e);
+            app.conf.spec = {};
+        }
+    }
+    if(!app.conf.spec.swagger) {
+        app.conf.spec.swagger = '2.0';
+    }
+    if(!app.conf.spec.info) {
+        app.conf.spec.info = {
+            version: app.info.version,
+            title: app.info.name,
+            description: app.info.description
+        };
+    }
+    app.conf.spec.info.version = app.info.version;
+    if(!app.conf.spec.paths) {
+        app.conf.spec.paths = {};
+    }
+
+    // set the CORS and CSP headers
+    app.all('*', function(req, res, next) {
+        if(app.conf.cors !== false) {
+            res.header('access-control-allow-origin', app.conf.cors);
+            res.header('access-control-allow-headers', 'accept, 
x-requested-with, content-type');
+            res.header('access-control-expose-headers', 'etag');
+        }
+        res.header('x-xss-protection', '1; mode=block');
+        res.header('x-content-type-options', 'nosniff');
+        res.header('x-frame-options', 'SAMEORIGIN');
+        res.header('content-security-policy', app.conf.csp);
+        res.header('x-content-security-policy', app.conf.csp);
+        res.header('x-webkit-csp', app.conf.csp);
+
+        sUtil.initAndLogRequest(req, app);
+
+        next();
+    });
+
+    // disable the X-Powered-By header
+    app.set('x-powered-by', false);
+    // disable the ETag header - users should provide them!
+    app.set('etag', false);
+    // enable compression
+    app.use(compression({level: app.conf.compression_level}));
+    // use the JSON body parser
+    app.use(bodyParser.json());
+    // use the application/x-www-form-urlencoded parser
+    app.use(bodyParser.urlencoded({extended: true}));
+
+    return BBPromise.resolve(app);
 
 }
 
@@ -129,40 +129,44 @@
  */
 function loadRoutes (app) {
 
-       // get the list of files in routes/
-       return fs.readdirAsync(__dirname + '/routes')
-       .map(function (fname) {
-               // ... and then load each route
-               // but only if it's a js file
-               if(!/\.js$/.test(fname)) {
-                       return;
-               }
-               // import the route file
-               var route = require(__dirname + '/routes/' + fname);
-               route = route(app);
-               // check that the route exports the object we need
-               if(route.constructor !== Object || !route.path || !route.router 
|| !(route.api_version || route.skip_domain)) {
-                       throw new TypeError('routes/' + fname + ' does not 
export the correct object!');
-               }
-               // wrap the route handlers with Promise.try() blocks
-               sUtil.wrapRouteHandlers(route.router, app);
-               // determine the path prefix
-               var prefix = '';
-               if(!route.skip_domain) {
-                       prefix = '/:domain/v' + route.api_version;
-               }
-               // all good, use that route
-               app.use(prefix + route.path, route.router);
-       }).then(function () {
-               // catch errors
-               sUtil.setErrorHandler(app);
-               // serve static files from static/ - here to allow routes to 
take precedence
-               app.use(express.static(__dirname + '/static'));
-               // route loading is now complete, return the app object
-               return BBPromise.resolve(app);
-       });
+    // get the list of files in routes/
+    return fs.readdirAsync(__dirname + '/routes').map(function(fname) {
+        return BBPromise.try(function () {
+            // ... and then load each route
+            // but only if it's a js file
+            if(!/\.js$/.test(fname)) {
+                return undefined;
+            }
+            // import the route file
+            var route = require(__dirname + '/routes/' + fname);
+            return route(app);
+        }).then(function (route) {
+            if(route === undefined) {
+                return undefined;
+            }
+            // check that the route exports the object we need
+            if (route.constructor !== Object || !route.path || !route.router 
|| !(route.api_version || route.skip_domain)) {
+                throw new TypeError('routes/' + fname + ' does not export the 
correct object!');
+            }
+            // wrap the route handlers with Promise.try() blocks
+            sUtil.wrapRouteHandlers(route.router);
+            // determine the path prefix
+            var prefix = '';
+            if(!route.skip_domain) {
+                prefix = '/:domain/v' + route.api_version;
+            }
+            // all good, use that route
+            app.use(prefix + route.path, route.router);
+        });
+    }).then(function () {
+        // catch errors
+        sUtil.setErrorHandler(app);
+        // route loading is now complete, return the app object
+        return BBPromise.resolve(app);
+    });
 
 }
+
 
 /**
  * Creates and start the service's web server
@@ -171,23 +175,24 @@
  */
 function createServer(app) {
 
-       // return a promise which creates an HTTP server,
-       // attaches the app to it, and starts accepting
-       // incoming client requests
-       var server;
-       return new BBPromise(function (resolve) {
-               server = http.createServer(app).listen(
-                       app.conf.port,
-                       app.conf.interface,
-                       resolve
-               );
-       }).then(function () {
-               app.logger.log('info',
-                       'Worker ' + process.pid + ' listening on ' + 
app.conf.interface + ':' + app.conf.port);
-               return server;
-       });
+    // return a promise which creates an HTTP server,
+    // attaches the app to it, and starts accepting
+    // incoming client requests
+    var server;
+    return new BBPromise(function (resolve) {
+        server = http.createServer(app).listen(
+            app.conf.port,
+            app.conf.interface,
+            resolve
+        );
+    }).then(function () {
+        app.logger.log('info',
+            'Worker ' + process.pid + ' listening on ' + app.conf.interface + 
':' + app.conf.port);
+        return server;
+    });
 
 }
+
 
 /**
  * The service's entry point. It takes over the configuration
@@ -197,9 +202,14 @@
  */
 module.exports = function(options) {
 
-       return initApp(options)
-       .then(loadRoutes)
-       .then(createServer);
+    return initApp(options)
+    .then(loadRoutes)
+    .then(function(app) {
+        // serve static files from static/
+        app.use(express.static(__dirname + '/static'));
+        return app;
+    })
+    .then(createServer);
 
 };
 
diff --git a/config.dev.yaml b/config.dev.yaml
index 65423a9..b3ca3bc 100644
--- a/config.dev.yaml
+++ b/config.dev.yaml
@@ -34,6 +34,9 @@
       port: 1970
       # IP address to bind to, all IPs by default
       # interface: localhost # uncomment to only listen on localhost
+      # more per-service config settings
+      # the location of the spec, defaults to spec.yaml if not specified
+      # spec: ./spec.template.yaml
       # allow cross-domain requests to the API (default '*')
       cors: '*'
       # to disable use:
@@ -41,7 +44,21 @@
       # to restrict to a particular domain, use:
       # cors: restricted.domain.org
       # URL of the outbound proxy to use (complete with protocol)
-      # proxy: http://127.0.0.0.1
+      # proxy: http://my.proxy.org:8080
+      # the list of domains for which not to use the proxy defined above
+      # no_proxy_list:
+      #   - domain1.com
+      #   - domain2.org
+      # the list of incoming request headers that can be logged; if left empty,
+      # the following headers are allowed: cache-control, content-length,
+      # content-type, if-match, user-agent, x-request-id
+      # log_header_whitelist:
+      #   - cache-control
+      #   - content-length
+      #   - content-type
+      #   - if-match
+      #   - user-agent
+      #   - x-request-id
       # User-Agent HTTP header to use for requests
       userAgent: null
       # URL where to contact Zotero
diff --git a/config.prod.yaml b/config.prod.yaml
new file mode 100644
index 0000000..d593c66
--- /dev/null
+++ b/config.prod.yaml
@@ -0,0 +1,35 @@
+# Number of worker processes to spawn.
+# Set to 0 to run everything in a single process without clustering.
+# Use 'ncpu' to run as many workers as there are CPU units
+num_workers: ncpu
+
+# Log error messages and gracefully restart a worker if v8 reports that it
+# uses more heap (note: not RSS) than this many mb.
+worker_heap_limit_mb: 500
+
+# Logger info
+logging:
+  level: warn
+  streams:
+  # Use gelf-stream -> logstash
+  - type: gelf
+    host: logstash1003.eqiad.wmnet
+    port: 12201
+
+# Statsd metrics reporter
+metrics:
+  type: statsd
+  host: statsd.eqiad.wmnet
+  port: 8125
+
+services:
+  - name: service-template-node
+    # a relative path or the name of an npm package, if different from name
+    module: ./app.js
+    # optionally, a version constraint of the npm package
+    # version: ^0.4.0
+    # per-service config
+    conf:
+      port: 6927
+      # interface: localhost # uncomment to only listen on localhost
+      # more per-service config settings
diff --git a/dist/init-scripts/systemd.erb b/dist/init-scripts/systemd.erb
new file mode 100644
index 0000000..78b5be3
--- /dev/null
+++ b/dist/init-scripts/systemd.erb
@@ -0,0 +1,25 @@
+[Unit]
+Description=<%= @description ? @service_name + ' - ' + @description : 
@service_name %>
+Documentation=<%= @homepage %>
+After=network.target local-fs.target
+
+[Service]
+Type=simple
+LimitNOFILE=<%= @no_file %>
+PIDFile=%t/<%= @service_name %>.pid
+User=<%= @service_name %>
+Group=<%= @service_name %>
+WorkingDirectory=/srv/deployment/<%= @service_name %>/deploy
+Environment="NODE_PATH='/srv/deployment/<%= @service_name 
%>/deploy/node_modules'" "<%= @service_name.gsub(/[^a-z0-9_]/, '_').upcase 
%>_PORT=<%= @port %>"
+ExecStart=/usr/bin/nodejs src/server.js -c /etc/<%= @service_name 
%>/config.yaml
+Restart=always
+RestartSec=5
+StandardOutput=syslog
+StandardError=syslog
+SyslogIdentifier=<%= @service_name %>
+TimeoutStartSec=5
+TimeoutStopSec=60
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/dist/init-scripts/sysvinit.erb b/dist/init-scripts/sysvinit.erb
new file mode 100644
index 0000000..f889596
--- /dev/null
+++ b/dist/init-scripts/sysvinit.erb
@@ -0,0 +1,172 @@
+#!/bin/sh
+### BEGIN INIT INFO
+# Provides:          <%= @service_name %>
+# Required-Start:    $local_fs $network $remote_fs $syslog
+# Required-Stop:     $local_fs $network $remote_fs $syslog
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: <%= @description || @service_name %>
+# Description:       <%= @description ? @service_name + ' - ' + @description : 
@service_name %>
+### END INIT INFO
+
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC="<%= @service_name %> service"
+NAME=<%= @service_name %>
+SCRIPT_PATH=/srv/deployment/$NAME/deploy/src/server.js
+DAEMON="/usr/bin/nodejs $SCRIPT_PATH"
+DAEMON_ARGS="-c /etc/$NAME/config.yaml"
+PIDFILE=/var/run/$NAME.pid
+SCRIPTNAME=/etc/init.d/$NAME
+
+# Exit if the package is not installed
+[ -e "$SCRIPT_PATH" ] || exit 0
+
+# Read configuration variable file if it is present
+# NOTE: only the DAEMON_ARGS var should be set in the file if present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+
+# export some variables into the process' environment
+export PORT
+export INTERFACE
+export NODE_PATH=/srv/deployment/$NAME/deploy/node_modules
+export <%= @service_name.gsub(/[^a-z0-9_]/, '_').upcase %>_PORT=<%= @port %>
+
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
+# and status_of_proc is working.
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+       # up the number of fds [sockets] from 1024
+       ulimit -n <%= @no_file %>
+
+       # Return
+       #   0 if daemon has been started
+       #   1 if daemon was already running
+       #   2 if daemon could not be started
+
+       start-stop-daemon --start --quiet --pidfile $PIDFILE -bm \
+               -c $NAME:$NAME --test \
+                --exec /bin/sh -- \
+                -c "$DAEMON $DAEMON_ARGS 2>&1 | logger -i -t $NAME" \
+               || return 1
+       start-stop-daemon --start --quiet --pidfile $PIDFILE -bm \
+               -c <%= @service_name %>:<%= @service_name %> \
+                --exec /bin/sh -- \
+                -c "$DAEMON $DAEMON_ARGS 2>&1 | logger -i -t $NAME" \
+               || return 2
+       echo "Started <%= @service_name %> service on port <%= @port %>"
+
+
+       # Add code here, if necessary, that waits for the process to be ready
+       # to handle requests from services started subsequently which depend
+       # on this one.  As a last resort, sleep for some time.
+       sleep 5
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+       # Return
+       #   0 if daemon has been stopped
+       #   1 if daemon was already stopped
+       #   2 if daemon could not be stopped
+       #   other if a failure occurred
+       start-stop-daemon --stop --quiet --retry=TERM/60/KILL/5 --pidfile 
$PIDFILE --name $NAME
+       RETVAL="$?"
+       [ "$RETVAL" = 2 ] && return 2
+       # Wait for children to finish too if this is a daemon that forks
+       # and if the daemon is only ever run from this initscript.
+       # If the above conditions are not satisfied then add some other code
+       # that waits for the process to drop all resources that could be
+       # needed by services started subsequently.  A last resort is to
+       # sleep for some time.
+       start-stop-daemon --stop --quiet --oknodo --retry=0/5/KILL/5 --exec 
$DAEMON
+       [ "$?" = 2 ] && return 2
+       # Many daemons don't delete their pidfiles when they exit.
+       rm -f $PIDFILE
+       return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+do_reload() {
+       #
+       # If the daemon can reload its configuration without
+       # restarting (for example, when it is sent a SIGHUP),
+       # then implement that here.
+       #
+       start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name 
$NAME
+       return 0
+}
+
+case "$1" in
+  start)
+       [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+       do_start
+       case "$?" in
+               0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+               2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+       esac
+       ;;
+  stop)
+       [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+       do_stop
+       case "$?" in
+               0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+               2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+       esac
+       ;;
+  status)
+       status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+       ;;
+  #reload|force-reload)
+       #
+       # If do_reload() is not implemented then leave this commented out
+       # and leave 'force-reload' as an alias for 'restart'.
+       #
+       #log_daemon_msg "Reloading $DESC" "$NAME"
+       #do_reload
+       #log_end_msg $?
+       #;;
+  restart|force-reload)
+       #
+       # If the "reload" option is implemented then remove the
+       # 'force-reload' alias
+       #
+       log_daemon_msg "Restarting $DESC" "$NAME"
+       do_stop
+       case "$?" in
+         0|1)
+               do_start
+               case "$?" in
+                       0) log_end_msg 0 ;;
+                       1) log_end_msg 1 ;; # Old process is still running
+                       *) log_end_msg 1 ;; # Failed to start
+               esac
+               ;;
+         *)
+               # Failed to stop
+               log_end_msg 1
+               ;;
+       esac
+       ;;
+  *)
+       echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
+       exit 3
+       ;;
+esac
+
diff --git a/dist/init-scripts/upstart.erb b/dist/init-scripts/upstart.erb
new file mode 100644
index 0000000..4cffcb5
--- /dev/null
+++ b/dist/init-scripts/upstart.erb
@@ -0,0 +1,24 @@
+# Upstart job for <%= @service_name %>
+
+description "<%= @description ? @service_name + ' - ' + @description : 
@service_name %>"
+
+start on (local-filesystems and net-device-up IFACE!=lo)
+stop on runlevel [!2345]
+
+# up ulimit -n a bit
+limit nofile <%= @no_file %> <%= @no_file %>
+
+setuid "<%= @service_name %>"
+setgid "<%= @service_name %>"
+
+env NODE_PATH="/srv/deployment/<%= @service_name %>/deploy/node_modules"
+env <%= @service_name.gsub(/[^a-zA-Z0-9_]/, '_').upcase %>_PORT="<%= @port %>"
+
+respawn
+
+# wait 60 seconds for a graceful restart before killing the master
+kill timeout 60
+
+chdir /srv/deployment/<%= @service_name %>/deploy
+exec /usr/bin/nodejs src/server.js -c /etc/<%= @service_name %>/config.yaml >> 
/var/log/<%= @service_name %>/main.log 2>&1
+
diff --git a/lib/util.js b/lib/util.js
index c6e4aed..868765e 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -13,29 +13,29 @@
  */
 function HTTPError(response) {
 
-       Error.call(this);
-       Error.captureStackTrace(this, HTTPError);
+    Error.call(this);
+    Error.captureStackTrace(this, HTTPError);
 
-       if(response.constructor !== Object) {
-               // just assume this is just the error message
-               var msg = response;
-               response = {
-                       status: 500,
-                       type: 'internal_error',
-                       title: 'InternalError',
-                       detail: msg
-               };
-       }
+    if(response.constructor !== Object) {
+        // just assume this is just the error message
+        var msg = response;
+        response = {
+            status: 500,
+            type: 'internal_error',
+            title: 'InternalError',
+            detail: msg
+        };
+    }
 
-       this.name = this.constructor.name;
-       this.message = response.status + '';
-       if(response.type) {
-               this.message += ': ' + response.type;
-       }
+    this.name = this.constructor.name;
+    this.message = response.status + '';
+    if(response.type) {
+        this.message += ': ' + response.type;
+    }
 
-       for (var key in response) {
-               this[key] = response[key];
-       }
+    for (var key in response) {
+        this[key] = response[key];
+    }
 
 }
 
@@ -45,21 +45,32 @@
 /**
  * Generates an object suitable for logging out of a request object
  *
- * @param {Request} the request
+ * @param {Request} req          the request
+ * @param {RegExp}  whitelistRE  the RegExp used to filter headers
  * @return {Object} an object containing the key components of the request
  */
-function reqForLog(req) {
+function reqForLog(req, whitelistRE) {
 
-       return {
-               url: req.originalUrl,
-               headers: req.headers,
-               method: req.method,
-               params: req.params,
-               query: req.query,
-               body: req.body,
-               remoteAddress: req.connection.remoteAddress,
-               remotePort: req.connection.remotePort
-       };
+    var ret = {
+        url: req.originalUrl,
+        headers: {},
+        method: req.method,
+        params: req.params,
+        query: req.query,
+        body: req.body,
+        remoteAddress: req.connection.remoteAddress,
+        remotePort: req.connection.remotePort
+    };
+
+    if(req.headers && whitelistRE) {
+        Object.keys(req.headers).forEach(function(hdr) {
+            if(whitelistRE.test(hdr)) {
+                ret.headers[hdr] = req.headers[hdr];
+            }
+        });
+    }
+
+    return ret;
 
 }
 
@@ -67,17 +78,17 @@
 /**
  * Serialises an error object in a form suitable for logging
  *
- * @param {Error} the error to serialise
+ * @param {Error} err error to serialise
  * @return {Object} the serialised version of the error
  */
 function errForLog(err) {
 
-       var ret = bunyan.stdSerializers.err(err);
-       ret.status = err.status;
-       ret.type = err.type;
-       ret.detail = err.detail;
+    var ret = bunyan.stdSerializers.err(err);
+    ret.status = err.status;
+    ret.type = err.type;
+    ret.detail = err.detail;
 
-       return ret;
+    return ret;
 
 }
 
@@ -88,10 +99,9 @@
  */
 function generateRequestId() {
 
-       return uuid.TimeUuid.now().toString();
+    return uuid.TimeUuid.now().toString();
 
 }
-
 
 
 /**
@@ -100,26 +110,21 @@
  * regardless of whether a handler returns/uses promises
  * or not.
  *
- * @param {Router} the router object
- * @param {Application} the application object
+ * @param {Router} router object
  */
-function wrapRouteHandlers(router, app) {
+function wrapRouteHandlers(router) {
 
-       router.stack.forEach(function(routerLayer) {
-               routerLayer.route.stack.forEach(function(layer) {
-                       var origHandler = layer.handle;
-                       layer.handle = function(req, res, next) {
-                               BBPromise.try(function() {
-                                       req.headers = req.headers || {};
-                                       req.headers['x-request-id'] = 
req.headers['x-request-id'] || generateRequestId();
-                                       req.logger = 
app.logger.child({request_id: req.headers['x-request-id']});
-                                       req.logger.log('trace/req', {req: 
reqForLog(req), msg: 'incoming request'});
-                                       return origHandler(req, res, next);
-                               })
-                               .catch(next);
-                       };
-               });
-       });
+    router.stack.forEach(function(routerLayer) {
+        routerLayer.route.stack.forEach(function(layer) {
+            var origHandler = layer.handle;
+            layer.handle = function(req, res, next) {
+                BBPromise.try(function() {
+                    return origHandler(req, res, next);
+                })
+                .catch(next);
+            };
+        });
+    });
 
 }
 
@@ -132,76 +137,76 @@
  */
 function setErrorHandler(app) {
 
-       app.use(function(err, req, res, next) {
-               var errObj;
-               // ensure this is an HTTPError object
-               if(err.constructor === HTTPError) {
-                       errObj = err;
-               } else if(err instanceof Error) {
-                       // is this an HTTPError defined elsewhere? (preq)
-                       if(err.constructor.name === 'HTTPError') {
-                               var o = { status: err.status };
-                               if(err.body && err.body.constructor === Object) 
{
-                                       
Object.keys(err.body).forEach(function(key) {
-                                               o[key] = err.body[key];
-                                       });
-                               } else {
-                                       o.detail = err.body;
-                               }
-                               o.message = err.message;
-                               errObj = new HTTPError(o);
-                       } else {
-                               // this is a standard error, convert it
-                               errObj = new HTTPError({
-                                       status: 500,
-                                       type: 'internal_error',
-                                       title: err.name,
-                                       detail: err.message,
-                                       stack: err.stack
-                               });
-                       }
-               } else if(err.constructor === Object) {
-                       // this is a regular object, suppose it's a response
-                       errObj = new HTTPError(err);
-               } else {
-                       // just assume this is just the error message
-                       errObj = new HTTPError({
-                               status: 500,
-                               type: 'internal_error',
-                               title: 'InternalError',
-                               detail: err
-                       });
-               }
-               // ensure some important error fields are present
-               if(!errObj.status) { errObj.status = 500; }
-               if(!errObj.type) { errObj.type = 'internal_error'; }
-               // add the offending URI and method as well
-               if(!errObj.method) { errObj.method = req.method; }
-               if(!errObj.uri) { errObj.uri = req.url; }
-               // some set 'message' or 'description' instead of 'detail'
-               errObj.detail = errObj.detail || errObj.message || 
errObj.description || '';
-               // adjust the log level based on the status code
-               var level = 'error';
-               if(Number.parseInt(errObj.status) < 400) {
-                       level = 'trace';
-               } else if(Number.parseInt(errObj.status) < 500) {
-                       level = 'info';
-               }
-               // log the error
-               (req.logger || app.logger).log(level + '/' +
-                               (errObj.component ? errObj.component : 
errObj.status),
-                               errForLog(errObj));
-               // let through only non-sensitive info
-               var respBody = {
-                       status: errObj.status,
-                       type: errObj.type,
-                       title: errObj.title,
-                       detail: errObj.detail,
-                       method: errObj.method,
-                       uri: errObj.uri
-               };
-               res.status(errObj.status).json(respBody);
-       });
+    app.use(function(err, req, res, next) {
+        var errObj;
+        // ensure this is an HTTPError object
+        if(err.constructor === HTTPError) {
+            errObj = err;
+        } else if(err instanceof Error) {
+            // is this an HTTPError defined elsewhere? (preq)
+            if(err.constructor.name === 'HTTPError') {
+                var o = { status: err.status };
+                if(err.body && err.body.constructor === Object) {
+                    Object.keys(err.body).forEach(function(key) {
+                        o[key] = err.body[key];
+                    });
+                } else {
+                    o.detail = err.body;
+                }
+                o.message = err.message;
+                errObj = new HTTPError(o);
+            } else {
+                // this is a standard error, convert it
+                errObj = new HTTPError({
+                    status: 500,
+                    type: 'internal_error',
+                    title: err.name,
+                    detail: err.message,
+                    stack: err.stack
+                });
+            }
+        } else if(err.constructor === Object) {
+            // this is a regular object, suppose it's a response
+            errObj = new HTTPError(err);
+        } else {
+            // just assume this is just the error message
+            errObj = new HTTPError({
+                status: 500,
+                type: 'internal_error',
+                title: 'InternalError',
+                detail: err
+            });
+        }
+        // ensure some important error fields are present
+        if(!errObj.status) { errObj.status = 500; }
+        if(!errObj.type) { errObj.type = 'internal_error'; }
+        // add the offending URI and method as well
+        if(!errObj.method) { errObj.method = req.method; }
+        if(!errObj.uri) { errObj.uri = req.url; }
+        // some set 'message' or 'description' instead of 'detail'
+        errObj.detail = errObj.detail || errObj.message || errObj.description 
|| '';
+        // adjust the log level based on the status code
+        var level = 'error';
+        if(Number.parseInt(errObj.status) < 400) {
+            level = 'trace';
+        } else if(Number.parseInt(errObj.status) < 500) {
+            level = 'info';
+        }
+        // log the error
+        (req.logger || app.logger).log(level + '/' +
+                (errObj.component ? errObj.component : errObj.status),
+                errForLog(errObj));
+        // let through only non-sensitive info
+        var respBody = {
+            status: errObj.status,
+            type: errObj.type,
+            title: errObj.title,
+            detail: errObj.detail,
+            method: errObj.method,
+            uri: errObj.uri
+        };
+        res.status(errObj.status).json(respBody);
+    });
 
 }
 
@@ -214,25 +219,40 @@
  */
 function createRouter(opts) {
 
-       var options = {
-               mergeParams: true
-       };
+    var options = {
+        mergeParams: true
+    };
 
-       if(opts && opts.constructor === Object) {
-               Object.keys(opts).forEach(function(key) {
-                       options[key] = opts[key];
-               });
-       }
+    if(opts && opts.constructor === Object) {
+        Object.keys(opts).forEach(function(key) {
+            options[key] = opts[key];
+        });
+    }
 
-       return express.Router(options);
+    return express.Router(options);
 
 }
 
 
+/**
+ * Adds logger to the request and logs it
+ *
+ * @param {*} req request object
+ * @param {Application} app application object
+ */
+function initAndLogRequest(req, app) {
+    req.headers = req.headers || {};
+    req.headers['x-request-id'] = req.headers['x-request-id'] || 
generateRequestId();
+    req.logger = app.logger.child({request_id: req.headers['x-request-id']});
+    req.logger.log('trace/req', {req: reqForLog(req, 
app.conf.log_header_whitelist), msg: 'incoming request'});
+}
+
+
 module.exports = {
-       HTTPError: HTTPError,
-       wrapRouteHandlers: wrapRouteHandlers,
-       setErrorHandler: setErrorHandler,
-       router: createRouter
+    HTTPError: HTTPError,
+    initAndLogRequest: initAndLogRequest,
+    wrapRouteHandlers: wrapRouteHandlers,
+    setErrorHandler: setErrorHandler,
+    router: createRouter
 };
 
diff --git a/package.json b/package.json
index 8642adf..bbb0c94 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,8 @@
   "version": "0.3.1",
   "description": "Converts search terms such as URL or DOI into citations.",
   "homepage": "https://www.mediawiki.org/wiki/Citoid";,
+  "license": "Apache-2.0",
+  "main": "./app.js",
   "scripts": {
     "start": "service-runner",
     "test": "mocha test/index.js",
@@ -10,30 +12,30 @@
   },
   "dependencies": {
     "bluebird": "2.8.2",
-    "body-parser": "1.13.3",
+    "body-parser": "1.14.1",
     "bunyan": "1.5.1",
     "cassandra-uuid": "0.0.2",
     "cheerio": "0.19.0",
-    "compression": "1.5.2",
+    "compression": "1.6.0",
     "content-type": "1.0.1",
     "express": "4.13.3",
     "html-metadata": "1.2.1",
     "iconv-lite": "0.4.11",
     "ip": "1.0.1",
-    "js-yaml": "3.4.2",
+    "js-yaml": "3.4.3",
     "preq": "0.4.4",
     "request": "^2.58.0",
-    "service-runner": "0.2.5",
+    "service-runner": "0.2.11",
     "tough-cookie": "2.0.0",
     "striptags": "2.0.3"
   },
   "devDependencies": {
     "extend": "3.0.0",
-    "istanbul": "0.3.17",
-    "mocha": "2.2.5",
+    "istanbul": "0.3.22",
+    "mocha": "2.3.3",
     "mocha-jshint": "2.2.3",
-    "mocha-lcov-reporter": "0.0.2",
-    "swagger-router": "0.1.1"
+    "mocha-lcov-reporter": "1.0.0",
+    "swagger-router": "0.2.0"
   },
   "deploy": {
     "target": "ubuntu",
diff --git a/routes/citoid.js b/routes/citoid.js
index 3f33db4..8f01279 100644
--- a/routes/citoid.js
+++ b/routes/citoid.js
@@ -3,6 +3,7 @@
 
 var sUtil = require('../lib/util');
 var CitoidRequest = require('../lib/CitoidRequest.js');
+var CitoidService = require('../lib/CitoidService');
 
 /**
  * The main router object
@@ -95,6 +96,20 @@
 
     app = appObj;
 
+    // set allowed export formats and expected response type
+    app.nativeFormats = {
+        'mediawiki':'application/json',
+        'zotero':'application/json',
+        'mwDeprecated':'application/json'
+    };
+    app.zoteroFormats = {
+        'bibtex':'application/x-bibtex'
+    };
+    app.formats = Object.assign({}, app.nativeFormats, app.zoteroFormats);
+
+    // init the Citoid service object
+    app.citoid = new CitoidService(app);
+
     return {
         path: '/',
         skip_domain: true,
diff --git a/scripts/gen-init-scripts.rb b/scripts/gen-init-scripts.rb
new file mode 100755
index 0000000..e523b97
--- /dev/null
+++ b/scripts/gen-init-scripts.rb
@@ -0,0 +1,71 @@
+#!/usr/bin/env ruby
+
+
+require 'erb'
+require 'json'
+require 'yaml'
+
+
+rootdir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
+indir = File.join(rootdir, 'dist', 'init-scripts')
+outdir = indir
+
+
+class ScriptData
+
+  include ERB::Util
+
+  @@suffix = {'systemd' => '.service', 'upstart' => '.conf'}
+  
+  def initialize input_dir
+    @template = {}
+    self.init input_dir
+  end
+
+  def set_info root_dir
+    self.read_info(root_dir).each do |key, value|
+      self.instance_variable_set "@#{key}".to_sym, value
+    end
+    @service_name = @name
+    @no_file ||= 10000
+  end
+
+  def generate output_dir
+    @template.each do |name, erb|
+      File.open(File.join(output_dir, "#{@name}#{@@suffix[name]}"), 'w') do 
|io|
+        io.write erb.result(binding())
+      end
+    end
+  end
+
+  def init input_dir
+    Dir.glob(File.join(input_dir, '*.erb')).each do |fname|
+      @template[File.basename(fname, '.erb')] = ERB.new(File.read(fname))
+    end
+  end
+
+  def read_info root_dir
+    data = YAML.load(File.read(File.join(root_dir, 
'config.yaml')))['services'][0]['conf']
+    return data.merge(JSON.load(File.read(File.join(root_dir, 
'package.json'))))
+  end
+
+end
+
+
+if ARGV.size > 0 and ['-h', '--help'].include? ARGV[0]
+  puts 'This is a simple script to generate various service init scripts'
+  puts 'Usage: gen-init-scripts.rb [output_dir]'
+  exit 1
+elsif ARGV.size > 0
+  outdir = ARGV[0]
+end
+
+unless File.directory? outdir
+  STDERR.puts 'The output directory must exist! Aborting...'
+  exit 2
+end
+
+data = ScriptData.new indir
+data.set_info rootdir
+data.generate outdir
+
diff --git a/static/index.html b/static/index.html
index 3b0f8db..c0ae1c8 100644
--- a/static/index.html
+++ b/static/index.html
@@ -12,4 +12,4 @@
                <input type="hidden" name="format" value="mediawiki" />
                <p>URL: <input name="search" size="100" 
value="http://link.springer.com/chapter/10.1007/11926078_68"; /> <input 
type="submit" /></p>
        </form>
-</body></html>
\ No newline at end of file
+</body></html>
diff --git a/test/features/app/spec.js b/test/features/app/spec.js
index 9c685fa..570fb40 100644
--- a/test/features/app/spec.js
+++ b/test/features/app/spec.js
@@ -1,12 +1,12 @@
 'use strict';
 
 
-var preq = require('preq');
+var preq   = require('preq');
 var assert = require('../../utils/assert.js');
 var server = require('../../utils/server.js');
-var URI = require('swagger-router').URI;
-var yaml = require('js-yaml');
-var fs = require('fs');
+var URI    = require('swagger-router').URI;
+var yaml   = require('js-yaml');
+var fs     = require('fs');
 
 
 function staticSpecLoad() {
diff --git a/test/utils/assert.js b/test/utils/assert.js
index 644d60a..73871e4 100644
--- a/test/utils/assert.js
+++ b/test/utils/assert.js
@@ -149,15 +149,14 @@
 }
 
 
-module.exports.ok               = assert.ok;
-module.exports.fails            = fails;
-module.exports.deepEqual        = deepEqual;
-module.exports.isDeepEqual      = isDeepEqual;
-module.exports.notDeepEqual     = notDeepEqual;
-module.exports.contentType      = contentType;
-module.exports.status           = status;
+module.exports.ok             = assert.ok;
+module.exports.fails          = fails;
+module.exports.deepEqual      = deepEqual;
+module.exports.isDeepEqual    = isDeepEqual;
+module.exports.notDeepEqual   = notDeepEqual;
+module.exports.contentType    = contentType;
+module.exports.status         = status;
 module.exports.checkError       = checkError;
 module.exports.checkCitation    = checkCitation;
 module.exports.checkZotCitation = checkZotCitation;
 module.exports.checkBibtex      = checkBibtex;
-
diff --git a/test/utils/logStream.js b/test/utils/logStream.js
index 2440e15..f8e292d 100644
--- a/test/utils/logStream.js
+++ b/test/utils/logStream.js
@@ -1,72 +1,68 @@
 'use strict';
 
-
 var bunyan = require('bunyan');
-
 
 function logStream(logStdout) {
 
-    var log = [];
-    var parrot = bunyan.createLogger({
-        name: 'test-logger',
-        level: 'warn'
-    });
+  var log = [];
+  var parrot = bunyan.createLogger({
+    name: 'test-logger',
+    level: 'warn'
+  });
 
-    function write(chunk, encoding, callback) {
-        try {
-            var entry = JSON.parse(chunk);
-            var levelMatch = /^(\w+)/.exec(entry.levelPath);
-            if (logStdout && levelMatch) {
-                var level = levelMatch[1];
-                if (parrot[level]) {
-                    parrot[level](entry);
-                }
+  function write(chunk, encoding, callback) {
+    try {
+        var entry = JSON.parse(chunk);
+        var levelMatch = /^(\w+)/.exec(entry.levelPath);
+        if (logStdout && levelMatch) {
+            var level = levelMatch[1];
+            if (parrot[level]) {
+                parrot[level](entry);
             }
-        } catch (e) {
-            console.error('something went wrong trying to parrot a log entry', 
e, chunk);
         }
-
-        log.push(chunk);
+    } catch (e) {
+        console.error('something went wrong trying to parrot a log entry', e, 
chunk);
     }
 
-    // to implement the stream writer interface
-    function end(chunk, encoding, callback) {
+    log.push(chunk);
+  }
+
+  // to implement the stream writer interface
+  function end(chunk, encoding, callback) {
+  }
+
+  function get() {
+    return log;
+  }
+
+  function slice() {
+
+    var begin = log.length;
+    var end   = null;
+
+    function halt() {
+      if (end === null) {
+        end = log.length;
+      }
     }
 
     function get() {
-        return log;
-    }
-
-    function slice() {
-
-        var begin = log.length;
-        var end   = null;
-
-        function halt() {
-            if (end === null) {
-                end = log.length;
-            }
-        }
-
-        function get() {
-            return log.slice(begin, end);
-        }
-
-        return {
-            halt: halt,
-            get: get
-        };
-
+      return log.slice(begin, end);
     }
 
     return {
-        write: write,
-        end: end,
-        slice: slice,
-        get: get
+      halt: halt,
+      get: get
     };
 
-}
+  }
 
+  return {
+    write: write,
+    end: end,
+    slice: slice,
+    get: get
+  };
+}
 
 module.exports = logStream;
diff --git a/test/utils/server.js b/test/utils/server.js
index b811768..15576b4 100644
--- a/test/utils/server.js
+++ b/test/utils/server.js
@@ -1,6 +1,10 @@
 'use strict';
 
 
+// mocha defines to avoid JSHint breakage
+/* global describe, it, before, beforeEach, after, afterEach */
+
+
 var BBPromise = require('bluebird');
 var ServiceRunner = require('service-runner');
 var logStream = require('./logStream');
@@ -32,9 +36,9 @@
 // make a deep copy of it for later reference
 var origConfig = extend(true, {}, config);
 
-var stop    = function () {};
+var stop = function () {};
 var options = null;
-var runner  = new ServiceRunner();
+var runner = new ServiceRunner();
 
 
 function start(_options) {
@@ -88,9 +92,6 @@
 }
 
 
-module.exports = {
-    config: config,
-    start: start,
-    query: query
-};
-
+module.exports.config = config;
+module.exports.start  = start;
+module.exports.query  = query;

-- 
To view, visit https://gerrit.wikimedia.org/r/247274
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Iffd68d0a2dfc806d471e64535027ee4004a7d4af
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/services/citoid
Gerrit-Branch: master
Gerrit-Owner: Mobrovac <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to