001/*
002 *  $URL: $
003 *  $Author: $
004 *  $Revision: $
005 *  $Date: $
006 */
007/*
008 *
009 *  Written by Tom Gutwin - WebARTS Design.
010 *  Copyright (C) 2013 WebARTS Design, North Vancouver Canada
011 *  http://www.webarts.bc.ca
012 *
013 *  This program is free software; you can redistribute it and/or modify
014 *  it under the terms of the GNU General Public License as published by
015 *  the Free Software Foundation; either version 2 of the License, or
016 *  (at your option) any later version.
017 *
018 *  This program is distributed in the hope that it will be useful,
019 *  but WITHOUT ANY WARRANTY; without_ even the implied warranty of
020 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
021 *  GNU General Public License for more details.
022 *
023 *  You should have received a copy of the GNU General Public License
024 *  along with this program; if not, write to the Free Software
025 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
026 */
027package ca.bc.webarts.tools.gapi;
028
029import com.google.api.client.auth.oauth2.Credential;
030import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
031import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
032import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
033import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
034import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets.Details;
035import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
036import com.google.api.client.googleapis.media.MediaHttpDownloader;
037import com.google.api.client.googleapis.media.MediaHttpUploader;
038import com.google.api.client.googleapis.media.MediaHttpDownloaderProgressListener;
039import com.google.api.client.googleapis.media.MediaHttpUploaderProgressListener;
040import com.google.api.client.http.FileContent;
041import com.google.api.client.http.GenericUrl;
042import com.google.api.client.http.HttpTransport;
043import com.google.api.client.json.JsonFactory;
044import com.google.api.client.json.jackson2.JacksonFactory;
045import com.google.api.client.util.Preconditions;
046import com.google.api.client.util.store.DataStoreFactory;
047import com.google.api.client.util.store.FileDataStoreFactory;
048import com.google.api.services.drive.Drive;
049import com.google.api.services.drive.DriveScopes;
050import com.google.api.services.drive.model.File;
051
052import java.io.FileOutputStream;
053import java.io.IOException;
054import java.io.InputStreamReader;
055import java.io.OutputStream;
056import java.security.GeneralSecurityException;
057import java.text.NumberFormat;
058import java.util.Collections;
059import java.util.List;
060import java.util.Properties;
061import java.util.Vector;
062
063import com.aftexsw.util.bzip.BZip;
064
065/**
066 * A simple abstract application that sends and receives files against the Drive API v2 and authenticates using OAuth 2.0.
067 * <ul>
068 * <li><a href="https://developers.google.com/api-client-library/java/apis/drive/v2">
069 *  https://developers.google.com/api-client-library/java/apis/drive/v2</a></li>
070 * <li><a href="https://google-developers.appspot.com/drive/quickstart-java">
071 *  Java Drive Quickstart</a></li>
072 * <li><a href="https://developers.google.com/accounts/docs/OAuth2">
073 * https://developers.google.com/accounts/docs/OAuth2</a></li></ul>
074 *  See also the javadoc
075 *  <a href="https://developers.google.com/resources/api-libraries/documentation/drive/v2/java/latest/">
076 *  https://developers.google.com/resources/api-libraries/documentation/drive/v2/java/latest/</a><br /><br />
077 * <b>You will have to get your own OAuth registraion secrets</b> and enter them in the appropriate class varables before use.
078 * See the OAuth link above for instructions.<br /><hr />
079 * It provides the basic authentication and send and receive methods that an extending class can use and extend.<br /><br />
080 * <b>all you need to do is:</b>
081 *   <pre>
082 *  class GDriveCrypter extends GDriver
083 *  {
084 *    // .... whatever
085 *   public static void main(String[] args)
086 *   {
087 *     String fileNameToUpload = "someFile.txt";
088 *     GDriveCrypter instance = new GDriveCrypter(fileNameToUpload);            // super is provided by GDriver
089 *     //Authorize GDrive
090 *     instance.initDrive(instance.authorize());                                // provided by GDriver
091 *
092 *     // Encrypt
093 *    String encryptedFilename = instance.encrypt(instance.getloadFilePath());
094 *     if (encryptedFilename!=null && !encryptedFilename.equals(""))
095 *     {
096 *       // Send File to GDrive
097 *       instance.setloadFilePath(encryptedFilename);                           // provided by GDriver
098 *       File uploadedFile = instance.uploadFile(false);                        // provided by GDriver
099 *     }
100 *   }
101 *  }
102 *   </pre>
103 *
104 **/
105public abstract class GDriver
106{
107  /**  A holder for this clients System File Separator.  */
108  public final static String SYSTEM_FILE_SEPERATOR = java.io.File.separator;
109
110  /**  A holder for this clients System line termination separator.  */
111  public final static String SYSTEM_LINE_SEPERATOR =
112                                           System.getProperty("line.separator");
113  /** Class constant holding an application name for use by logger or whatever. **/
114  protected static final String APPLICATION_NAME = "WebARTSDesign-GDriver/0.1";
115  /** Class constant holding a default location for downloading into. **/
116  protected static final String DEFAULT_DOWNLOAD_DIR = ".";
117  /** Class constant holding a default filename to  download into. **/
118  protected static final String DEFAULT_LOAD_FILENAME = "./gDriveDocument.txt";
119  /** Class constant holding a defaultdatastore directory. **/
120  protected static final String DEFAULT_DATASTORE_DIR = System.getProperty("user.home")+
121                                                      SYSTEM_FILE_SEPERATOR+
122                                                      ".store"+
123                                                      SYSTEM_FILE_SEPERATOR+
124                                                      "GDrive";
125
126  /**  The VM classpath (used in some methods)..  */
127  public static String CLASSPATH = System.getProperty("class.path");
128
129  /**  The users home ditrectory.  */
130  public static String USERHOME = System.getProperty("user.home");
131
132  /**  The users pwd ditrectory.  */
133  public static String USERDIR = System.getProperty("user.dir");
134
135  /**  A holder This classes name (used when logging).  */
136  protected static String CLASSNAME ;
137
138
139  /** Classvar holding the GoogleClientSecrets for this and extended classes.**/
140  protected static GoogleClientSecrets clientSecrets_ = null;  //new GoogleClientSecrets();
141
142  /** Classvar holding the GDrive logine secrets specific to your login ID**/
143  protected String loadFilePath_ = DEFAULT_LOAD_FILENAME;
144  /** Classvar holding the directory to download into.**/
145  protected String dirForDownload_ = DEFAULT_DOWNLOAD_DIR;
146  protected java.io.File loadFile_ = new java.io.File(loadFilePath_);
147
148  /** Directory to store user credentials. */
149  protected  java.io.File dataStoreDir_ = null;
150
151  /**
152   * Global instance of the {@link DataStoreFactory}. The best practice is to make it a single
153   * globally shared instance across your application.
154   */
155  protected FileDataStoreFactory dataStoreFactory_ = null;
156
157  /** Class instance of the HTTP transport. */
158  protected HttpTransport httpTransport_ = null;
159
160  /** Class instance of the JSON factory. */
161  protected JsonFactory jsonFactory_ = null;
162
163  /** Class Drive API client. */
164  protected Drive drive_ = null;
165
166
167  /** Default constructor that gets all the basic class settimgs setup and gives you a class instance to do the work.
168   *  Then all you need to do is:
169   *  <pre>
170   *  GDriver instance = new GDriveCrypter(); // uses the DEFAULT_LOAD_FILENAME
171   *  //Authorize GDrive
172   *  instance.initDrive(instance.authorize());
173   *
174   *  // Encrypt
175   * String encryptedFilename = instance.encrypt(instance.getloadFilePath());
176   *  if (encryptedFilename!=null && !encryptedFilename.equals(""))
177   *  {
178   *    // Send File to GDrive
179   *    instance.setloadFilePath(encryptedFilename);
180   *    File uploadedFile = instance.uploadFile(false);
181   *  }
182   *  </pre>
183   *
184   **/
185  public  GDriver() throws GeneralSecurityException, IOException
186  {
187    loadFilePath_ = DEFAULT_LOAD_FILENAME;
188    dirForDownload_  = DEFAULT_DOWNLOAD_DIR;
189    loadFile_ = new java.io.File(loadFilePath_);
190    dataStoreDir_ = new java.io.File(DEFAULT_DATASTORE_DIR);
191    httpTransport_ = GoogleNetHttpTransport.newTrustedTransport();
192
193    dataStoreFactory_ = new FileDataStoreFactory(dataStoreDir_);
194    jsonFactory_ = JacksonFactory.getDefaultInstance();
195  }
196
197
198  /** Constructor that gets all the basic class settimgs setup, setsup the filename to upload and gives you a class instance to do the work.
199   *  Then all you need to do is:
200   *  <pre>
201   *  String fileNameToUpload = "someFile.txt";
202   *  GDriveCrypter instance = new GDriveCrypter(fileNameToUpload);
203
204   *  //Authorize GDrive
205   *  instance.initDrive(instance.authorize());
206   *
207   *  // Encrypt
208   * String encryptedFilename = instance.encrypt(instance.getloadFilePath());
209   *  if (encryptedFilename!=null && !encryptedFilename.equals(""))
210   *  {
211   *    // Send File to GDrive
212   *    instance.setloadFilePath(encryptedFilename);
213   *    File uploadedFile = instance.uploadFile(false);
214   *  }
215   *  </pre>
216   *
217   **/
218  public  GDriver(String filenameToLoad) throws GeneralSecurityException, IOException
219  {
220    loadFilePath_ = filenameToLoad;
221    dirForDownload_  = DEFAULT_DOWNLOAD_DIR;
222    loadFile_ = new java.io.File(loadFilePath_);
223    dataStoreDir_ = new java.io.File(DEFAULT_DATASTORE_DIR);
224    httpTransport_ = GoogleNetHttpTransport.newTrustedTransport();
225
226    dataStoreFactory_ = new FileDataStoreFactory(dataStoreDir_);
227    jsonFactory_ = JacksonFactory.getDefaultInstance();
228  }
229
230  /** Set the filename path for the file that will be uploaded. **/
231  protected void setloadFilePath(String path)
232  {
233      loadFilePath_ = path;
234      loadFile_ = new java.io.File(loadFilePath_);
235  }
236
237
238  /** get the filename path for the file that will be uploaded. **/
239  protected String getloadFilePath() { return loadFilePath_; }
240
241
242  /** Initializes, with the passed in credentials, the Drive class instance -drive_, that gets used for the target upload.
243   * Use the authorize method to get yuor credentials.
244   * @param cred is the Credential to use for the upload.
245   **/
246  protected void initDrive(Credential cred)
247  {
248      // set up the class Drive instance
249      drive_ = new Drive.Builder(httpTransport_,
250                                jsonFactory_,
251                                cred
252                                ).setApplicationName(APPLICATION_NAME).build();
253  }
254
255  /** Loads the Google secrets / api usage authentication properties for this class. **/
256  protected GoogleClientSecrets initSecrets() throws IOException
257  {
258    boolean readFromJSON = true;
259
260    if(readFromJSON)
261    {
262        java.io.InputStream is = GDriveCrypter.class.getResourceAsStream("gapi.secretsFile");
263        clientSecrets_ = GoogleClientSecrets.load( jsonFactory_,
264                                                  new InputStreamReader(is)
265                                                  );
266    }
267    else
268    {
269        //GoogleClientSecrets.Details secretDetails = clientSecrets_.getDetails();
270        GoogleClientSecrets.Details secretDetails = new GoogleClientSecrets.Details();
271
272        secretDetails.setClientId("gapi.clientId");
273        secretDetails.setAuthUri("gapi.authUri");
274        secretDetails.setClientSecret("gapi.clientSecret");
275        secretDetails.setTokenUri("gapi.tokenUri");
276        Vector <String> v = new <String> Vector();
277        v.add("gapi.redirectUris.1");
278        v.add("gapi.redirectUris.2");
279        secretDetails.setRedirectUris(v);
280        secretDetails.set("auth_provider_x509_cert_url","https://www.googleapis.com/oauth2/v1/certs");
281        secretDetails.set("client_x509_cert_url","");
282        secretDetails.set("client_email","");
283        clientSecrets_.setInstalled(secretDetails);
284    }
285    return clientSecrets_;
286  }
287
288
289  /** Authorizes the installed application to access user's protected data. */
290  protected Credential authorize() throws Exception
291  {
292    // load client secrets
293    initSecrets();
294
295    /*
296    if (clientSecrets_.getDetails().getClientId().startsWith("Enter") ||
297        clientSecrets_.getDetails().getClientSecret().startsWith("Enter "))
298    {
299      System.out.println ("Enter Client ID and Secret from https://code.google.com/apis/console/?api=drive " + "into drive-cmdline-sample/src/main/resources/client_secrets.json");
300      System.exit(1);
301    }
302    */
303
304    // set up authorization code flow
305    GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder( httpTransport_,
306                                                                                jsonFactory_,
307                                                                                clientSecrets_,
308                                                                                Collections.singleton(DriveScopes.DRIVE_FILE)
309                                                                               ).setDataStoreFactory(dataStoreFactory_).build();
310    // authorize
311    return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
312  }
313
314
315  /** recursively creates parent dirs and the requested dir **/
316  public static void ensureFolderExists(java.io.File folder)
317  {
318    if ((folder != null) && !(folder.isDirectory() ))
319    {
320      ensureFolderExists(folder.getParentFile());
321      boolean suc = folder.mkdir();
322    }
323  }
324
325
326  /** UnBzips the passed file (that must have a '.bz2' extension) to its same filename
327   *  in the same dir  without the '.bz2' extension.
328   * @param  fo  The File object to unzip - must have a '.bz2' extension.
329   **/
330  public static long unBzip2It(java.io.File fo)
331  {
332    final String methodName = CLASSNAME + ".bzip2It(File)";
333    //logger_.debug("Entering " + methodName);
334    long retVal = 0L;
335
336    String fullFilename = fo.getAbsolutePath().trim();
337      java.io.File outFile = null;
338    if (fullFilename.lastIndexOf(".bz2")>-1)
339    {
340      String unzippedFilename = fullFilename.substring(0, fullFilename.lastIndexOf(".bz2"));
341      outFile = new java.io.File(unzippedFilename);
342      BZip.decompress(fo, outFile);
343      }
344  retVal = outFile.length();
345
346    //logger_.debug("Exiting " + methodName);
347    return retVal;
348  }
349
350
351  /**
352   *  Wrapper method to accept a dir or individual file in the passed in File
353   *  object AND then calls the method that writes the passed file/dir to this
354   *  MultiZip instances zip output stream. This is a recursive function. If the
355   *  passed File object is a file then it calls the function to write to zip
356   *  output stream. If it is a directory it gets the list of file objects in
357   *  the child directory and recurses on them.
358   *
359   * @param  fo  The File object to zip up (can be an actual file or dir).
360   * @return     the new length in bytes of the zipped file
361   */
362  public static long bzip2It(java.io.File fo)
363  {
364    final String methodName = CLASSNAME + ".bzip2It(File)";
365    //logger_.debug("Entering " + methodName);
366    long retVal = 0L;
367
368    String fullFilename = fo.getAbsolutePath().trim();
369    if (!fo.isDirectory())
370    {
371      //logger_.info("Bzip2ing "+fullFilename);
372        StringBuffer outLocation = new StringBuffer();
373          // zip it in place
374          //logger_.info("Bzip2ing "+fullFilename);
375          java.io.File outFile = new java.io.File(fullFilename + ".bz2");
376
377          ensureFolderExists(outFile.getParentFile());
378          try
379          {
380            BZip.compress(fo, outFile, 9);
381            //compressedFilenames_.add(fullFilename + ".bz2");
382            retVal += outFile.length();
383          }
384          catch (java.lang.ArithmeticException mathEx)
385          {
386            //logger_.error("BZip2 ERROR: " + fullFilename);
387            //logger_.error(mathEx.getMessage());
388          }
389    }
390    else
391    {
392        //logger_.info("Recursing " + fo.getPath());
393        String srcFileNames[] = fo.list();
394        for (int i = 0; i < srcFileNames.length; i++)
395        {
396          retVal += bzip2It(new java.io.File(fullFilename + java.io.File.separator +
397              srcFileNames[i]));
398        }
399    }
400
401    //logger_.debug("Exiting " + methodName);
402    return retVal;
403  }
404
405
406  /** Uploads a file using either resumable or direct media upload. */
407  protected File uploadFile(boolean useDirectUpload) throws IOException
408  {
409    File retVal = null;
410    if (drive_!=null)
411    {
412      File fileMetadata = new File();
413      fileMetadata.setTitle(loadFile_.getName());
414
415      //FileContent mediaContent = new FileContent("image/jpeg", loadFile_);
416      FileContent mediaContent = new FileContent("text/plain", loadFile_);
417
418      Drive.Files.Insert insert = drive_.files().insert(fileMetadata, mediaContent);
419      MediaHttpUploader uploader = insert.getMediaHttpUploader();
420      uploader.setDirectUploadEnabled(useDirectUpload);
421      uploader.setProgressListener(new FileUploadProgressListener());
422      retVal = insert.execute();
423    }
424    return retVal;
425  }
426
427
428  /** Updates the name of the uploaded file to have a "drivetest-" prefix. */
429  private File updateFileWithTestSuffix(String id) throws IOException
430  {
431    File retVal = null;
432    if (drive_!=null)
433    {
434      File fileMetadata = new File();
435      fileMetadata.setTitle("drivetest-" + loadFile_.getName());
436
437      Drive.Files.Update update = drive_.files().update(id, fileMetadata);
438      retVal = update.execute();
439    }
440    return retVal;
441  }
442
443
444  /** Downloads a file using either resumable or direct media download. */
445  private void downloadFile(boolean useDirectDownload, java.io.File fileToDownload) throws IOException
446  {
447    if (drive_!=null)
448    {
449      // create parent directory (if necessary)
450      java.io.File parentDir = new java.io.File(dirForDownload_);
451      if (!parentDir.exists() && !parentDir.mkdirs())
452      {
453        throw new IOException("Unable to create parent directory");
454      }
455      OutputStream out = new FileOutputStream(new java.io.File(parentDir, loadFilePath_));
456
457      MediaHttpDownloader downloader = new MediaHttpDownloader(httpTransport_, drive_.getRequestFactory().getInitializer());
458      downloader.setDirectDownloadEnabled(useDirectDownload);
459      downloader.setProgressListener(new FileDownloadProgressListener());
460     // downloader.download(new GenericUrl(uploadedFile.getDownloadUrl()), out);
461    }
462  }
463}
464
465
466/**
467 * Utility methods to print to the command line.
468 */
469class View {
470
471  static void header1(String name) {
472    System.out.println();
473    System.out.println("================== " + name + " ==================");
474    System.out.println();
475  }
476
477  static void header2(String name) {
478    System.out.println();
479    System.out.println("~~~~~~~~~~~~~~~~~~ " + name + " ~~~~~~~~~~~~~~~~~~");
480    System.out.println();
481  }
482}
483
484
485/**
486 * The File Upload Progress Listener.
487 *
488 * @author rmistry@google.com (Ravi)
489 */
490class FileUploadProgressListener implements MediaHttpUploaderProgressListener {
491
492  @Override
493  public void progressChanged(MediaHttpUploader uploader) throws IOException {
494    switch (uploader.getUploadState()) {
495      case INITIATION_STARTED:
496        View.header2("Upload Initiation has started.");
497        break;
498      case INITIATION_COMPLETE:
499        View.header2("Upload Initiation is Complete.");
500        break;
501      case MEDIA_IN_PROGRESS:
502        View.header2("Upload is In Progress: "
503            + NumberFormat.getPercentInstance().format(uploader.getProgress()));
504        break;
505      case MEDIA_COMPLETE:
506        View.header2("Upload is Complete!");
507        break;
508    }
509  }
510}
511
512
513/**
514 * The File Download Progress Listener.
515 *
516 * @author rmistry@google.com (Ravi)
517 */
518class FileDownloadProgressListener implements MediaHttpDownloaderProgressListener {
519
520  @Override
521  public void progressChanged(MediaHttpDownloader downloader) {
522    switch (downloader.getDownloadState()) {
523      case MEDIA_IN_PROGRESS:
524        View.header2("Download is in progress: " + downloader.getProgress());
525        break;
526      case MEDIA_COMPLETE:
527        View.header2("Download is Complete!");
528        break;
529    }
530  }
531}