View Javadoc

1   /*
2    * Copyright (C) 2017-2019 Centre National d'Etudes Spatiales (CNES).
3    *
4    * This library is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Lesser General Public
6    * License as published by the Free Software Foundation; either
7    * version 3.0 of the License, or (at your option) any later version.
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12   * Lesser General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this library; if not, write to the Free Software
16   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
17   * MA 02110-1301  USA
18   */
19  package fr.cnes.doi.settings;
20  
21  import fr.cnes.doi.exception.DoiRuntimeException;
22  import fr.cnes.doi.plugin.PluginFactory;
23  import fr.cnes.doi.security.UtilsCryptography;
24  import fr.cnes.doi.server.Starter;
25  import fr.cnes.doi.utils.DOIProperties;
26  import fr.cnes.doi.utils.Utils;
27  import fr.cnes.doi.utils.spec.Requirement;
28  import java.io.File;
29  import java.io.FileInputStream;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.OutputStream;
33  import java.io.UnsupportedEncodingException;
34  import java.net.URLDecoder;
35  import java.util.Enumeration;
36  import java.util.Map.Entry;
37  import java.util.Properties;
38  import java.util.concurrent.ConcurrentHashMap;
39  import org.apache.logging.log4j.Level;
40  import org.apache.logging.log4j.LogManager;
41  import org.apache.logging.log4j.Logger;
42  import org.restlet.data.LocalReference;
43  import org.restlet.representation.Representation;
44  import org.restlet.resource.ClientResource;
45  
46  /**
47   * Singleton to load and use the defined variables in the config.properties.
48   *
49   * @author Jean-Christophe (jean-christophe.malapert@cnes.fr)
50   */
51  @Requirement(reqId = Requirement.DOI_CONFIG_010, reqName = Requirement.DOI_CONFIG_010_NAME)
52  public final class DoiSettings {
53  
54      /**
55       * Configuration files in JAR.
56       */
57      public static final String CONFIG_PROPERTIES = "config.properties";
58  
59      /**
60       * Logger.
61       */
62      private static final Logger LOG = LogManager.getLogger(DoiSettings.class.getName());
63  
64      /**
65       * Test DOI : {@value #INIST_TEST_DOI}.
66       */
67      private static final String INIST_TEST_DOI = "10.5072";
68  
69      /**
70       * Settings loaded in memory.
71       */
72      private static final ConcurrentHashMap<String, String> MAP_PROPERTIES = new ConcurrentHashMap<>();
73  
74      /**
75       * Access to unique INSTANCE of Settings
76       *
77       * @return the configuration instance.
78       */
79      public static DoiSettings getInstance() {
80          LOG.traceEntry();
81          return LOG.traceExit(DoiSettingsHolder.INSTANCE);
82      }
83  
84      /**
85       * Secret key to decrypt login and password.
86       */
87      private String secretKey = UtilsCryptography.DEFAULT_SECRET_KEY;
88  
89      /**
90       * Path where the Java application inputStream located.
91       */
92      private String pathApp;
93  
94      /**
95       * private constructor Loads the defautl configuration properties
96       * {@value #CONFIG_PROPERTIES}
97       */
98      private DoiSettings() {
99          final Properties properties = loadConfigurationFile();
100         init(properties, Level.OFF);
101     }
102 
103     /**
104      * Loads configuration file and set it in memory.
105      *
106      * @param properties Configuration file
107      * @param level LOG level
108      */
109     private void init(final Properties properties, Level level) {
110         if (Level.OFF.equals(level)) {
111             level = null; // this fixes a bug in log4j2
112         }
113         LOG.traceEntry("Parameter\n\tproperties:{}\n\tlevel:{}", properties, level);
114         LOG.log(level, "----- DOI parameters ----");
115         fillConcurrentMap(properties, level);
116         computePathOfTheApplication();
117         PluginFactory.init(DoiSettings.MAP_PROPERTIES);
118         final Enumeration<String> keys = DoiSettings.MAP_PROPERTIES.keys();
119         while (keys.hasMoreElements()) {
120             final String key = keys.nextElement();
121             LOG.log(
122                     level, "{} = {}",
123                     key,
124                     isPassword(String.valueOf(key))
125                     ? Utils.transformPasswordToStars(String.valueOf(DoiSettings.MAP_PROPERTIES.get(
126                             key)))
127                     : DoiSettings.MAP_PROPERTIES.get(key)
128             );
129         }
130         LOG.log(level, "DOI settings have been loaded");
131         LOG.log(level, "-------------------------");
132         LOG.log(level, properties.getProperty(Consts.NAME) + " loaded");
133         LOG.traceExit();
134     }
135 
136     /**
137      * Computes path of the application.
138      */
139     private void computePathOfTheApplication() {
140         LOG.traceEntry();
141         try {
142             final String path = Starter.class.getProtectionDomain().getCodeSource().getLocation().
143                     getPath();
144             String decodedPath = URLDecoder.decode(path, "UTF-8");
145             final int posLastSlash = decodedPath.lastIndexOf('/');
146             decodedPath = decodedPath.substring(0, posLastSlash);
147             this.pathApp = decodedPath;
148         } catch (UnsupportedEncodingException ex) {
149             throw LOG.throwing(new DoiRuntimeException(ex));
150         }
151         LOG.traceExit();
152     }
153 
154     /**
155      * Load configuration file.
156      *
157      * @return the configuration file content
158      */
159     private Properties loadConfigurationFile() {
160         LOG.traceEntry();
161         final Properties properties = new DOIProperties();
162         final ClientResource client = new ClientResource(LocalReference.createClapReference(
163                 "class/config.properties"));
164         final Representation configurationFile = client.get();
165         try {
166             LOG.debug("Loading " + CONFIG_PROPERTIES + " by default");
167             properties.load(configurationFile.getStream());
168         } catch (IOException e) {
169             LOG.fatal("Unable to load " + CONFIG_PROPERTIES);
170             throw LOG.throwing(new DoiRuntimeException("Unable to load class/config.properties", e));
171         } finally {
172             client.release();
173         }
174         return LOG.traceExit(properties);
175     }
176 
177     /**
178      * Validates the configuration file.
179      */
180     public void validConfigurationFile() {
181         LOG.traceEntry();
182         final StringBuilder validation = new StringBuilder();
183         final String message = "Sets ";
184         if (isNotExist(DoiSettings.MAP_PROPERTIES, Consts.INIST_DOI)) {
185             validation.append(message).append(Consts.INIST_DOI).append("\n");
186         }
187         if (isNotExist(DoiSettings.MAP_PROPERTIES, Consts.INIST_LOGIN)) {
188             validation.append(message).append(Consts.INIST_LOGIN).append("\n");
189         }
190         if (isNotExist(DoiSettings.MAP_PROPERTIES, Consts.INIST_PWD)) {
191             validation.append(message).append(Consts.INIST_PWD).append("\n");
192         }
193         if (isNotExist(DoiSettings.MAP_PROPERTIES, Consts.SERVER_PROXY_TYPE)) {
194             validation.append(message).append(Consts.SERVER_PROXY_TYPE).append("\n");
195         }
196         if (isNotExist(DoiSettings.MAP_PROPERTIES, Consts.PLUGIN_PROJECT_SUFFIX)) {
197             validation.append(message).append(Consts.PLUGIN_PROJECT_SUFFIX).append("\n");
198         }
199         if (isNotExist(DoiSettings.MAP_PROPERTIES, Consts.PLUGIN_TOKEN)) {
200             validation.append(message).append(Consts.PLUGIN_TOKEN).append("\n");
201         }
202         if (isNotExist(DoiSettings.MAP_PROPERTIES, Consts.PLUGIN_USER_GROUP_MGT)) {
203             validation.append(message).append(Consts.PLUGIN_USER_GROUP_MGT).append("\n");
204         }
205 
206         if (isNotExist(DoiSettings.MAP_PROPERTIES, Consts.PLUGIN_AUTHENTICATION)) {
207             validation.append(message).append(Consts.PLUGIN_AUTHENTICATION).append("\n");
208         }
209 
210         validation.append(PluginFactory.getAuthenticationSystem().validate());
211         validation.append(PluginFactory.getProjectSuffix().validate());
212         validation.append(PluginFactory.getToken().validate());
213         validation.append(PluginFactory.getUserManagement().validate());
214 
215         if (validation.length() != 0) {
216             throw LOG.traceExit(new DoiRuntimeException(validation.toString()));
217         }
218         LOG.traceExit();
219     }
220 
221     /**
222      * Tests if the keyword exists in properties.
223      *
224      * @param properties configuration
225      * @param keyword keyword to test
226      * @return True when the keyword exists in configuration otherwise False
227      */
228     private boolean isExist(final ConcurrentHashMap<String, String> properties,
229             final String keyword) {
230         LOG.traceEntry("Parameters\n\tproperties : {}\n\tkeyword : {}", properties, keyword);
231         return LOG.traceExit(properties.containsKey(keyword) && !properties.get(keyword).isEmpty());
232     }
233 
234     /**
235      * Test if the keyword does not exist in properties
236      *
237      * @param properties configuration
238      * @param keyword keyword to test
239      * @return True when the keyword does not exist in configuration otherwise
240      * False
241      */
242     private boolean isNotExist(final ConcurrentHashMap<String, String> properties,
243             final String keyword) {
244         LOG.traceEntry("Parameters\n\tproperties : {}\n\tkeyword : {}", properties, keyword);
245         return LOG.traceExit(!isExist(properties, keyword));
246     }
247 
248     /**
249      * Sets the configuration as a MAP_PROPERTIES.
250      *
251      * @param properties the configuration file content
252      * @param level log level
253      */
254     private void fillConcurrentMap(final Properties properties, final Level level) {
255         LOG.traceEntry("Parameters\n\tproperties : {}\n\tlevel : {}", properties, level);
256         for (final Entry<Object, Object> entry : properties.entrySet()) {
257             MAP_PROPERTIES.put((String) entry.getKey(), (String) entry.getValue());
258         }
259         LOG.traceExit();
260     }
261 
262     /**
263      * Tests if the value of the key is a password
264      *
265      * @param key key to test
266      * @return True when the value of the key is a password otherwise false
267      */
268     private boolean isPassword(final String key) {
269         LOG.traceEntry("Parameter\n\tkey:{}", key);
270         return LOG.traceExit(Consts.INIST_PWD.equals(key)
271                 || PluginFactory.isPassword(Consts.PLUGIN_AUTHENTICATION, key)
272                 || PluginFactory.isPassword(Consts.PLUGIN_PROJECT_SUFFIX, key)
273                 || PluginFactory.isPassword(Consts.PLUGIN_TOKEN, key)
274                 || PluginFactory.isPassword(Consts.PLUGIN_USER_GROUP_MGT, key)
275                 || Consts.SERVER_HTTPS_KEYSTORE_PASSWD.equals(key)
276                 || Consts.SERVER_HTTPS_SECRET_KEY.equals(key) || Consts.SERVER_PROXY_PWD.equals(key)
277                 || Consts.SMTP_AUTH_PWD.equals(key) || Consts.TOKEN_KEY.equals(key));
278     }
279 
280     /**
281      * Tests if the key has a value.
282      *
283      * @param key key to test
284      * @return True when the value inputStream different or null and empty
285      */
286     public boolean hasValue(final String key) {
287         LOG.traceEntry("Parameter\n\tkey : {}", key);
288         return LOG.traceExit(Utils.isNotEmpty(getString(key)));
289     }
290 
291     /**
292      * Returns the secret key.
293      *
294      * @return the secret key.
295      */
296     public String getSecretKey() {
297         LOG.traceEntry();
298         return LOG.traceExit(this.secretKey);
299     }
300 
301     /**
302      * Sets the secret key.
303      *
304      * @param secretKey the secret key.
305      */
306     public void setSecretKey(final String secretKey) {
307         LOG.traceEntry("Parameter\n\t secretKey : {}", secretKey);
308         this.secretKey = secretKey;
309         LOG.traceExit();
310     }
311 
312     /**
313      * Returns the value of the key as string.
314      *
315      * @param key key to search
316      * @param defaultValue Default value if the key inputStream not found
317      * @return the value of the key
318      */
319     public String getString(final String key, final String defaultValue) {
320         LOG.traceEntry("Parameters\n\tkey : {}\n\tdefaultValue : {}", key, defaultValue);
321         return LOG.traceExit(MAP_PROPERTIES.getOrDefault(key, defaultValue));
322     }
323 
324     /**
325      * Returns the value of the key or null if no mapping for the key. A special
326      * processing inputStream done for the key
327      * {@value fr.cnes.doi.settings.Consts#INIST_DOI}. When this key inputStream
328      * called the value changes in respect of
329      * {@value fr.cnes.doi.settings.Consts#CONTEXT_MODE}. When
330      * {@value fr.cnes.doi.settings.Consts#CONTEXT_MODE} inputStream set to
331      * PRE_PROD, {@value fr.cnes.doi.settings.Consts#INIST_DOI} inputStream set
332      * to {@value #INIST_TEST_DOI}.
333      *
334      * @param key key to search
335      * @return the value of the key orl null if does not exist
336      */
337     public String getString(final String key) {
338         LOG.traceEntry("Parameter\n\tkey : {}", key);
339         final String value;
340         if (Consts.INIST_DOI.equals(key)) {
341             final String context = this.getString(Consts.CONTEXT_MODE, "DEV");
342             value = "PRE_PROD".equals(context) ? INIST_TEST_DOI : this.getString(Consts.INIST_DOI,
343                     null);
344         } else {
345             value = this.getString(key, null);
346         }
347         return LOG.traceExit(value);
348     }
349 
350     /**
351      * Returns the decoded value of the key. An exception inputStream raised
352      * when the key inputStream not encoded on 16bits.
353      *
354      * @param key key to search
355      * @return the decoded vale
356      */
357     public String getSecret(final String key) {
358         LOG.traceEntry("Parameter\n\tkey : {}", key);
359         final String result;
360         final String value = getString(key, "");
361         final boolean isEncrypted = Boolean.parseBoolean(
362                 this.getString(Consts.ENCRYPTED_FIELDS, "true"));
363         if (Utils.isEmpty(value)) {
364             result = value;
365         } else if (isEncrypted) {
366             result = UtilsCryptography.decrypt(value, getSecretKey());
367         } else {
368             result = value;
369         }
370         return LOG.traceExit(result);
371     }
372 
373     /**
374      * Returns the decoded value of the value. An exception inputStream raised
375      * when the key inputStream not encoded on 16bits.
376      *
377      * @param value value to decrypt
378      * @return the decoded vale
379      */
380     public String getSecretValue(final String value) {
381         LOG.traceEntry("Parameter\n\tvalue : {}", value);
382         final String result;
383         final boolean isEncrypted = Boolean.parseBoolean(
384                 this.getString(Consts.ENCRYPTED_FIELDS, "true"));
385         if (Utils.isEmpty(value)) {
386             result = value;
387         } else if (isEncrypted) {
388             result = UtilsCryptography.decrypt(value, getSecretKey());
389         } else {
390             result = value;
391         }
392         return LOG.traceExit(result);
393     }
394 
395     /**
396      * Returns the value of the key as an integer. NumberFormatException
397      * inputStream raisen when the value of the key in not compatible with an
398      * integer.
399      *
400      * @param key key to search
401      * @return the value
402      * @exception NumberFormatException if the string does not contain a
403      * parsable integer.
404      */
405     public int getInt(final String key) {
406         LOG.traceEntry("Parameter\n\tkey : {}", key);
407         return LOG.traceExit(Integer.parseInt(getString(key)));
408     }
409 
410     /**
411      * Returns the value of the key as an integer. NumberFormatException
412      * inputStream raisen when the value of the key in not compatible
413      *
414      * @param key key to search
415      * @param defaultValue default value
416      * @return the value
417      * @exception NumberFormatException if the string does not contain a
418      * parsable integer.
419      */
420     public int getInt(final String key, final String defaultValue) {
421         LOG.traceEntry("Parameter\n\tkey : {}\n\tdefaultValue", key, defaultValue);
422         return LOG.traceExit(Integer.parseInt(getString(key, defaultValue)));
423     }
424 
425     /**
426      * Returns the value of the key as a boolean. An exception inputStream
427      * raisen when the value of the key in not compatible with a boolean
428      *
429      * @param key key to search
430      * @return the value
431      * @exception IllegalArgumentException - if key not found
432      */
433     public boolean getBoolean(final String key) {
434         LOG.traceEntry("Parameter\n\tkey : {}", key);
435         if (getString(key) == null) {
436             throw LOG.throwing(Level.TRACE, new IllegalArgumentException("Key not found : " + key));
437         } else {
438             return LOG.traceExit(Boolean.parseBoolean(getString(key)));
439         }
440     }
441 
442     /**
443      * Returns the value of the key as a long. NumberFormatException inputStream
444      * raisen when the value of the key in not compatible
445      *
446      * @param key key to search
447      * @return the value
448      * @exception NumberFormatException - if the string does not contain a
449      * parsable long
450      */
451     public Long getLong(final String key) {
452         LOG.traceEntry("Parameter\n\tkey : {}", key);
453         return LOG.traceExit(Long.parseLong(getString(key)));
454     }
455 
456     /**
457      * Returns the value of the key as a long. NumberFormatException inputStream
458      * raisen when the value of the key in not compatible
459      *
460      * @param key key to search
461      * @param defaultValue default value
462      * @return the value
463      * @exception NumberFormatException - if the string does not contain a
464      * parsable long
465      */
466     public Long getLong(final String key, final String defaultValue) {
467         LOG.traceEntry("Parameter\n\tkey : {}\n\tdefaultValue", key, defaultValue);
468         return LOG.traceExit(Long.parseLong(getString(key, defaultValue)));
469     }
470 
471     /**
472      * Displays the configuration file.
473      */
474     public void displayConfigFile() {
475         LOG.traceEntry();
476         final ClientResource client = new ClientResource(LocalReference.createClapReference(
477                 "class/" + CONFIG_PROPERTIES));
478         final Representation configurationFile = client.get();
479         try {
480             copyStream(configurationFile.getStream(), System.out);
481         } catch (IOException ex) {
482             LOG.fatal("Cannot display the configuration file located to class/" + CONFIG_PROPERTIES,
483                     ex);
484         } finally {
485             client.release();
486         }
487         LOG.traceExit();
488     }
489 
490     /**
491      * Sets a custom properties file.
492      *
493      * @param path Path to the properties file
494      * @throws IOException - if an error occurred when reading from the input
495      * stream.
496      */
497     public void setPropertiesFile(final String path) throws IOException {
498         LOG.traceEntry("Parameter\n\tpath : {}", path);
499         try (InputStream inputStream = new FileInputStream(new File(path))) {
500             setPropertiesFile(inputStream);
501         }
502         LOG.traceExit();
503     }
504 
505     /**
506      * Sets a custom properties file.
507      *
508      * @param inputStream Input stream
509      * @throws java.io.IOException - if an error occurred when reading from the
510      * input stream.
511      */
512     public void setPropertiesFile(final InputStream inputStream) throws IOException {
513         LOG.traceEntry("With an inputstream");
514         final Properties properties = new DOIProperties();
515         properties.load(inputStream);
516         init(properties, Level.INFO);
517         // the following singletons depend on DoiSettings.
518         // So if we init DoiSettings, we need to init the following
519         // singletons
520         ProxySettings.getInstance().init();
521         EmailSettings.getInstance().init();
522         LOG.traceExit();
523     }
524 
525     /**
526      * Returns the path of the application on the file system.
527      *
528      * @return the path of the application on the file system
529      */
530     public String getPathApp() {
531         LOG.traceEntry();
532         return LOG.traceExit(this.pathApp);
533     }
534 
535     /**
536      * Copy input stream to output stream.
537      *
538      * @param inputStream input stream
539      * @param outputStream output stream
540      */
541     private void copyStream(final InputStream inputStream, final OutputStream outputStream) {
542         LOG.traceEntry("With an input and output stream");
543         final int bufferSize = 1024;
544         try {
545             final byte[] bytes = new byte[bufferSize];
546             for (;;) {
547                 final int count = inputStream.read(bytes, 0, bufferSize);
548                 if (count == -1) {
549                     break;
550                 }
551                 outputStream.write(bytes, 0, count);
552             }
553             inputStream.close();
554             outputStream.flush();
555             outputStream.close();
556         } catch (IOException ex) {
557             LOG.fatal("error when displaying the configuraiton file on the standard output", ex);
558         }
559         LOG.traceExit();
560     }
561 
562     /**
563      * Holder
564      */
565     private static class DoiSettingsHolder {
566 
567         /**
568          * Unique Instance unique not pre-initiliaze
569          */
570         private static final DoiSettings INSTANCE = new DoiSettings();
571     }
572 }