001/*
002 * Copyright (c) 2010 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2016-07-19 06:26:44 +0000 (Tue, 19 Jul 2016) $' 
007 * '$Revision: 34509 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029
030/**
031 * 
032 */
033package org.kepler.util;
034
035import java.io.BufferedReader;
036import java.io.File;
037import java.io.FileInputStream;
038import java.io.FileNotFoundException;
039import java.io.FileOutputStream;
040import java.io.IOException;
041import java.io.InputStream;
042import java.io.InputStreamReader;
043import java.io.ObjectInput;
044import java.io.ObjectInputStream;
045import java.io.ObjectOutputStream;
046import java.io.OutputStream;
047import java.io.OutputStreamWriter;
048import java.net.URL;
049import java.net.URLConnection;
050import java.net.URLEncoder;
051import java.util.Hashtable;
052import java.util.List;
053import java.util.Properties;
054import java.util.UUID;
055import java.util.Vector;
056
057import org.apache.commons.logging.Log;
058import org.apache.commons.logging.LogFactory;
059import org.kepler.build.modules.Module;
060import org.kepler.build.project.ProjectLocator;
061import org.kepler.configuration.ConfigurationManager;
062import org.kepler.configuration.ConfigurationProperty;
063import org.kepler.objectmanager.lsid.LSIDGenerator;
064
065/**
066 * This class contains an Authority and a Namespace to be used for uniquely
067 * generating KeplerLSIDs.
068 * 
069 * @author Aaron Schultz
070 */
071public class AuthNamespace {
072        private static final Log log = LogFactory.getLog(AuthNamespace.class
073                        .getName());
074        private static final boolean isDebugging = log.isDebugEnabled();
075
076        /**
077         * The authority that assigned the namespace to this Kepler instance.
078         */
079        private String _authority;
080
081        /**
082         * The namespace that uniquely identifies this Kepler instance along with
083         * the authority
084         */
085        private String _namespace;
086
087        /**
088         * The list of configured authorities to try to get a unique authNamespace
089         * from. This is populated from the config.xml file.
090         */
091        private Vector<String> _configuredAuthorities;
092
093        /**
094         * The filename where the authority and namesapce are stored. Store the
095         * AuthorizedNamespace file in the KEPLER directory, not the cache
096         * directory.
097         */
098        private String _anFileName;
099
100        private boolean _initialized = false;
101
102        private final String _saveFileName = "AuthorizedNamespace";
103
104        /** singleton instance **/
105        private static AuthNamespace instance = null;
106
107        /**
108         * Constructor.
109         */
110        protected AuthNamespace() {
111
112                File persistentDir = DotKeplerManager.getInstance()
113                                .getPersistentModuleDirectory("core");
114                _anFileName = persistentDir.toString();
115                if (!_anFileName.endsWith(File.separator)) {
116                        _anFileName += File.separator;
117                }
118                _anFileName += _saveFileName;
119                if (isDebugging)
120                        log.debug(_anFileName);
121                _configuredAuthorities = new Vector<String>(1);
122
123                // make sure the userDirPath exists
124                new File(DotKeplerManager.getDotKeplerPath()).mkdirs();
125
126        }
127
128        /**
129         * @return the Authority
130         */
131        public String getAuthority() {
132                return _authority;
133        }
134
135        private void setAuthority(String authority) throws Exception {
136                int ind;
137                if (authority.startsWith("http://")) {
138                        authority = authority.substring(7);
139                } else if (authority.startsWith("https://")) {
140                        authority = authority.substring(8);
141                } else if ( (ind = authority.indexOf("://")) >= 0) {
142                        authority = authority.substring(ind+3);
143                }
144                if (authority.indexOf(':') >= 0) {
145                        throw new Exception("authority can not contain a colon");
146                }
147                _authority = authority;
148        }
149
150        /**
151         * @return the Namespace
152         */
153        public String getNamespace() {
154                return _namespace;
155        }
156
157        private void setNamespace(String namespace) throws Exception {
158                if (namespace.indexOf(':') >= 0) {
159                        throw new Exception("namespace can not contain a colon");
160                }
161                _namespace = namespace;
162        }
163
164        public String getAuthNamespace() {
165                return getAuthority() + ":" + getNamespace();
166        }
167
168        /**
169         * The purpose here is to return a unique string based on the authNamespace
170         * string that can be used for including in the name of Instance specific
171         * strings such as the .kepler directory name. This is a one way operation
172         * such that the HashValue can be obtained from the AuthNamespace string but
173         * probably not the other way around.
174         * 
175         */
176        public String getFileNameForm() {
177                String filenameFriendly = getAuthNamespace();
178                filenameFriendly = filenameFriendly.replaceAll("/", ".");
179                filenameFriendly = filenameFriendly.replaceAll(":", ".");
180                filenameFriendly = filenameFriendly.replaceAll("\\.\\.", ".");
181                // log.debug(filenameFriendly);
182                return filenameFriendly;
183        }
184
185        /**
186         * Set the unique Authority and Namespace for this Kepler Instance.
187         */
188        public void initialize() {
189                log.debug("initialize()");
190                if (_initialized)
191                        return;
192                _initialized = true; // never do this twice.
193
194                /**
195                 * Configure the list of authNamespace services that should be used to
196                 * get a unique namespace from.
197                 */
198                readAuthNamespaceServicesConfiguration();
199
200                /*
201                 * If the AuthNamespace file does not exist, create it.
202                 */
203                File ianFile = new File(_anFileName);
204                if (!ianFile.exists()) {
205                        generateAuthNamespace();
206                }
207
208                refreshAuthNamespace();
209
210                if (getAuthority() == "uuid") {
211                        /*
212                         * TODO warn the user they are working with a uuid and ask if they'd
213                         * like to verify a new namespace from an authority.
214                         * 
215                         * This should be farmed out to a class in the gui module. If there
216                         * is no gui around then an alternate path should be created for
217                         * doing this.
218                         */
219                }
220        }
221        
222        /**
223         * Don't use this method.  Only if the LSID_GENERATOR table is corrupt
224         * or deleted do we want to get a new Authorized Namespace.
225         */
226        public void getNewAuthorizedNamespace() {
227                File anFile = new File(_anFileName);
228                anFile.delete();
229                generateAuthNamespace();
230                refreshAuthNamespace();
231        }
232
233        /**
234         * This initializer method is only to be used for JUnit testing.
235         */
236        public void initializeForTesting(String authority, String namespace) {
237                log.debug("initializeForTests()");
238                if (_initialized)
239                        return;
240                _initialized = true;
241
242                // use a different file for testing
243                _anFileName = ProjectLocator.getProjectDir().toString();
244                if (!_anFileName.endsWith(File.separator)) {
245                        _anFileName += File.separator;
246                }
247                _anFileName += "AuthorizedNamespaceTesting";
248                if (isDebugging)
249                        log.debug(_anFileName);
250
251                // use a generic authority and namespace for testing
252                _authority = authority;
253                _namespace = namespace;
254
255                try {
256                        writeAuthorizedNamespace();
257                } catch (Exception e) {
258                        System.out.println(e.toString());
259                        e.printStackTrace();
260                }
261                _initialized = true;
262        }
263
264        /**
265         * Populate the configuredAuthorities list from the config.xml file.
266         */
267        private void readAuthNamespaceServicesConfiguration() {
268                ConfigurationManager cm = ConfigurationManager.getInstance();
269                Module cmMod = ConfigurationManager.getModule("configuration-manager");
270                String path = "authNamespaceServices.authNamespaceService";
271                List<ConfigurationProperty> anProps = cm.getProperties(cmMod,
272                                path);
273                Hashtable<Integer,String> addys = new Hashtable<Integer,String>();
274                for (ConfigurationProperty anProp : anProps) {
275                        ConfigurationProperty ordProp = anProp.getProperty("ordering");
276                        ConfigurationProperty urlProp = anProp.getProperty("url");
277                        
278                        String ordStr = ordProp.getValue();
279                        Integer ordInt = Integer.parseInt(ordStr);
280                        String urlStr = urlProp.getValue();
281                        
282                        addys.put(ordInt,urlStr);
283                }
284                for (int i = 1; i <= addys.size(); i++) {
285                        Integer I = new Integer(i);
286                        String addy = addys.get(I);
287                        if (isDebugging) log.debug(addy);
288                        _configuredAuthorities.add(addy);
289                }
290        }
291
292        /**
293         * Try to query the configured authorities to get an assigned namespace.
294         * Otherwise generate a uuid as the namespace.
295         */
296        private void generateAuthNamespace() {
297                boolean verified = false;
298
299                /*
300                 * First, try to connect to a server to get a verified namespace.
301                 */
302                try {
303
304                        for (int i = 0; i < _configuredAuthorities.size(); i++) {
305                                String currentAuthority = _configuredAuthorities.get(i);
306                                verified = queryAuthorizedNamespace(currentAuthority);
307                                if (_authority == null || _namespace == null) {
308                                        verified = false;
309                                }
310                                if (verified) {
311                                        log.info("A unique namespace, " + _namespace
312                                                        + ", was successfully retrieved from authority "
313                                                        + _authority);
314                                        break;
315                                }
316                        }
317
318                } catch (Exception e1) {
319                        log.error(e1.toString());
320                        e1.printStackTrace();
321                }
322
323                /*
324                 * If a verified namespace cannot be retrieved, generate one.
325                 */
326                if (!verified) {
327                        /*
328                         * Generate a probabilistically unique random ID for the namespace
329                         * and set uuid as the authority.
330                         */
331                        log.warn("Generating UUID based AuthNamespace");
332
333                        try {
334                                setAuthority("uuid");
335                                setNamespace(UUID.randomUUID().toString());
336
337                                writeAuthorizedNamespace();
338                        } catch (Exception e) {
339                                log.error("Unable to write InstanceAuthNamespace file: "
340                                                + _anFileName);
341                                e.printStackTrace();
342                        }
343                }
344        }
345
346        /**
347         * Read in the Authority and Namespace from the AuthorizedNamespace file.
348         */
349        public void refreshAuthNamespace() {
350                if (isDebugging)
351                        log.debug("refreshAuthNamespace()");
352
353                try {
354                        InputStream is = null;
355                        ObjectInput oi = null;
356                        try {
357                                is = new FileInputStream(_anFileName);
358                                oi = new ObjectInputStream(is);
359                        Object newObj = oi.readObject();
360    
361                        String theString = (String) newObj;
362                        int firstColon = theString.indexOf(':');
363                        setAuthority(theString.substring(0, firstColon));
364                        setNamespace(theString.substring(firstColon + 1));
365                        } finally {
366                            if(oi != null) {
367                                oi.close();
368                            }
369                            if(is != null) {
370                                is.close();
371                            }
372                        }
373                } catch (FileNotFoundException e) {
374                        log.error(_saveFileName + " file was not found: " + _anFileName);
375                        e.printStackTrace();
376                } catch (IOException e) {
377                        log.error("Unable to create ObjectInputStream");
378                        e.printStackTrace();
379                } catch (ClassNotFoundException e) {
380                        log.error("Unable to read from ObjectInput");
381                        e.printStackTrace();
382                } catch (Exception e) {
383                        log.error(e.toString());
384                        e.printStackTrace();
385                }
386
387                if (isDebugging) {
388                        log.debug("Instance Authority: " + getAuthority());
389                        log.debug("Instance Namespace: " + getNamespace());
390                        log.debug("Instance AuthNamespace: " + getAuthNamespace());
391                        log.debug("Instance Hash Value: " + getFileNameForm());
392                }
393
394        }
395
396        /**
397         * Serialize the authority:namespace String to the AuthorizedNamespace file.
398         */
399        private void writeAuthorizedNamespace() throws Exception {
400                if (isDebugging)
401                        log.debug("writeAuthorizedNamespace()");
402
403                File ianFile = new File(_anFileName);
404                if (!ianFile.exists()) {
405                        ianFile.delete();
406                }
407                String authNamespace = getAuthNamespace();
408                if (authNamespace == null) {
409                        throw new Exception("authNamespace is null");
410                }
411                OutputStream os = new FileOutputStream(_anFileName);
412                ObjectOutputStream oos = null;
413                try {
414                    oos = new ObjectOutputStream(os);
415                    oos.writeObject(authNamespace);
416                    oos.flush();
417                } finally {
418                    if(oos != null) {
419                        oos.close();
420                    }
421                }
422
423                try {
424            // Every time we write a new AuthNamespace make sure there is
425            // at least one row added to the LSID_GENERATOR table for the
426            // new Authority and Namespace, this is how LSIDGenerator
427            // can tell if the LSID_GENERATOR table has been deleted since
428            // the last time it accessed it.
429            LSIDGenerator.insertRow(getAuthority(), getNamespace(), 0, 1);
430        } catch (Exception e) {
431            e.printStackTrace();
432        }
433    }
434
435        /**
436     * Return a unique namespace from the provided authority string.
437     * 
438         * @param authority
439         * @return boolean true if success
440         * @throws Exception
441         */
442        private boolean queryAuthorizedNamespace(String authority) throws Exception {
443                if (isDebugging)
444                        log.debug("queryAuthorizedNamespace( " + authority + ")");
445
446                // thwart malicious robots
447                String data = URLEncoder.encode("username", "UTF-8") + "="
448                                + URLEncoder.encode("kepler", "UTF-8"); 
449                data += "&" + URLEncoder.encode("password", "UTF-8") + "="
450                                + URLEncoder.encode("kepler", "UTF-8");
451
452                try {
453                        // Send the request
454                        URL url = new URL(authority);
455                        URLConnection conn = url.openConnection();
456                        conn.setDoOutput(true);
457                        OutputStream os = conn.getOutputStream();
458                        OutputStreamWriter wr = new OutputStreamWriter(os);
459                        wr.write(data);
460                        wr.flush();
461
462                        // Get the response as a set of Properties
463                        BufferedReader rd = new BufferedReader(new InputStreamReader(conn
464                                        .getInputStream()));
465                        String line;
466                        Properties props = new Properties();
467                        while ((line = rd.readLine()) != null) {
468                                if (isDebugging) log.debug(line);
469                                line = line.trim();
470                                if (line.equals(""))
471                                        continue;
472                                int equalsIndex = line.indexOf("=");
473                                if (equalsIndex > 0) {
474                                        String key = line.substring(0, equalsIndex).trim();
475                                        String value = line.substring(equalsIndex + 1).trim();
476                                        props.setProperty(key, value);
477                                }
478                        }
479                        wr.close();
480                        rd.close();
481
482                        // Check to see if there was an error
483                        String errorString = props.getProperty("error");
484                        if (errorString != null) {
485                                log.warn(errorString);
486                        } else {
487                                // Check and set the returned namespace
488                                String newNamespace = props.getProperty("namespace");
489                                if (newNamespace != null && !newNamespace.equals("")) {
490                                        setAuthority(authority);
491                                        setNamespace(newNamespace);
492
493                                        try {
494                                                writeAuthorizedNamespace();
495                                                return true;
496                                        } catch (Exception e) {
497                                                log.error("Unable to write " + _saveFileName
498                                                                + " file: " + _anFileName);
499                                                e.printStackTrace();
500                                        }
501                                }
502                        }
503                } catch (Exception e) {
504                        System.out.println(e.toString());
505                }
506
507                return false;
508        }
509
510        /**
511         * Method for getting an instance of this singleton class.
512         */
513        public static AuthNamespace getInstance() {
514                if (instance == null) {
515                        instance = new AuthNamespace();
516                        instance.initialize();
517                }
518                return instance;
519        }
520}