Showing posts with label google. Show all posts
Showing posts with label google. Show all posts

Saturday, 29 March 2014

Show private documents to your users from google drive without sharing them

In this post I will write how we can use google api to show content on our page from google drive, without actually changing the sharing settings for the document. We will use owner's oAuth access to proxy the content through our server.

Implementation:
I am using google drive sdk for java in this code snippet: https://developers.google.com/drive/v2/reference/
Given the fileId of a file uploaded on google drive we can query google to get the following info about file:

Alternate link: This is the link to file where the document can be accessed on google drive itself.
Download url : Link to download the original file. This is available only for the contetn uploaded on google drive. It is not available for the content created on google drive like google doc, google spreadsheet, google drive etc.
Export Links: links to export the documents in different formats. These links are only available for the content created on google drive like google doc, google spreadsheet, google drive etc. and are not available for content uploaded on google drive.

All of these links require google authentication and our end user doesn't have access to these documents. We are considering that our web application has been authorized(using oAuth) to access these documents by the document owner. Given these links we can make authenticated get request to these links on owner's behalf(by using access token) and serve this content to our end user by writing it to output stream. To make the authenticated get call we will use following code: Note that we are not reading all content in controller method in one go and then serving it, but we are actually buffering it in 8k chunks.


URL url = new URL(contentUrl)
URLConnection connection = url.openConnection();
connection.setRequestProperty("Authorization", 'Bearer ' + accessToken);

// this will actually buffer/stream the file in 8k chunks instead of reading the entire file into memory.
org.apache.commons.io.IOUtils.copy(connection.getInputStream(), response.outputStream)
Controller method to proxy the data and serve it on our page:
private static HttpTransport httpTransport = new NetHttpTransport()
private static JacksonFactory jsonFactory = new JacksonFactory();

//This is a controller method in our web-app
def viewGDOC(){
  String fileId = params.fileId
  String accessToken // read this from database using session
  String refreshToken // read this from database using session
  
  GoogleCredential gCredentials = new GoogleCredential.Builder()
                .setClientSecrets(CLIENT_ID, CLIENT_SECRET)
                .setJsonFactory(jsonFactory).setTransport(httpTransport).build()
                .setRefreshToken(refreshToken).setAccessToken(accessToken);
                
  Drive driveService = new Drive.Builder(httpTransport, jsonFactory, credentials).build();
  com.google.api.services.drive.model.File file = driveService.files().get(chapter.gDriveFileId).execute()
  
  String contentUrl
  def downloadUrl = file.getDownloadUrl()
  def pdfUrl = file.getExportLinks()?.get("application/pdf")
  if (downloadUrl) {
      contentUrl = downloadUrl
  } else {
      contentUrl = pdfUrl
  }
  
  URL url = new URL(contentUrl)
  URLConnection connection = url.openConnection();
  connection.setRequestProperty("Authorization", 'Bearer ' + googleCredential.accessToken);
  
  // this will actually buffer/stream the file in 8k chunks instead of reading the entire file into memory.
  org.apache.commons.io.IOUtils.copy(connection.getInputStream(), response.outputStream)
}
Once we get the content stream the next task is to show it to user. Since browsers can display only some mime types like image, video, pdf, we can't directly show many file other types like MS word documents. To better understand this upload a office format file like ppt to google drive and use the above code to proxy the content. You will see that the browser will not render proxied content  properly but instead download a zip which contains raw structure of file.
The solution is to use some kind of viewer that takes this type of content and converts it to html so that browser can display it. Think how google shows word file when you view it in google drive ? There are many web apps which take the url of file and gives back the html. e.g. : viewer.js, crocodoc, google viewer. We will use the google viewer here: https://docs.google.com/viewer
So if you are using iframe on your page to show the file content instead of pointing iframs's src to /viewGDoc , point it to https://docs.google.com/viewer?url='serverName/viewGDoc'&embedded=true . Please note that if you are using iframe the 'embedded=true' option is necessary in the url as without this the content that google viewer returns is non iframable(X Frame Options is sameorigin in that case).

Monday, 3 September 2012

Salesforce Mashup with google maps

This app helps user to plot  sales on the google map.The sales to be plotted can be filtered based on various criteria like location,size,type,date etc.Clicking on a sale on the map redirects to the corresponding account.
Final application looks like this :
you can install the unmanaged apex package from  here :
https://login.salesforce.com/packaging/installPackage.apexp?p0=04t90000000Pyoa

Use the following doc for deployment instructions :
https://docs.google.com/viewer?a=v&pid=sites&srcid=ZGVmYXVsdGRvbWFpbnxteWZpbGVzOTF8Z3g6NTJkN2U5YWU4ZTA3ZDZlMA

Demo video:
https://sites.google.com/site/myfiles91/home/files/salesmap.swf?attredirects=0

The app uses only one visual force page and one controller

Visual Force page :


<apex:page sidebar="false" Controller="salesData" id="page">

    <apex:includeScript value="{!URLFOR($Resource.jquery, 'jquery-1.3.2.min.js')}"/>
    <apex:includeScript value="{!URLFOR($Resource.jquery, 'jquery-ui-1.7.2.custom.min.js')}"/>
    <apex:stylesheet value="{!URLFOR($Resource.jquery,'jquery-ui-1.7.2.custom.css')}"/>
    <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
    
    <script type="text/javascript">    
        //This will load as soon as the page is ready and will setup slider
        $(document).ready(function(){
               
                //to setup slider
                $("#slider-range").slider({ //This line creates a slider on the DIV specified, options are passed arguments,comma separated below
                range: true,
                min: 0, //Min value for slider
                max: 400, //Max value for slider
                values: [0,250], //initial values of slider
                slide: function(event, ui){ //function to execute on slide event
                
                  document.getElementById('{!$Component.page.form.block.values.size_low}').value = ui.values[0]; //hidden fields to pass slider values to controller 
                  document.getElementById('{!$Component.page.form.block.values.size_high}').value = ui.values[1];
                  $("#amountValue").html(ui.values[0] + ' - ' + ui.values[1]); //display slider value as text
                  
                }
                });
                
                //to display initial values of slider
                $("#amountValue").html($("#slider-range").slider("values", 0) + ' - ' + $("#slider-range").slider("values", 1));
                
        
        });
        
    </script>
    <style>
        #map_canvas {
        font-family: Arial;
        font-size:12px;
        line-height:normal !important;
        height:500px;
        width:1000px;
        background:transparent;
        }
    </style>
                
                
     <apex:form id="form">
         <apex:pageBlock mode="edit" id="block">
               <apex:pageMessages />
               
               <apex:pageBlockSection columns="7" id="values" >
                
                  <apex:pageBlockSectionItem >
                    <apex:outputlabel value="* Country"/>
                    <apex:selectList value="{!country}" multiselect="false" required="true" size="1">
                       <apex:selectOptions value="{!countries}"/>
                    </apex:selectList>
                 </apex:pageBlockSectionItem>
                  
                 <apex:pageBlockSectionItem >
                   <apex:outputlabel value="State"/>
                   <apex:inputtext value="{!state}"/> 
                 </apex:pageBlockSectionItem>
                  
                 <apex:pageBlockSectionItem >  
                   <apex:outputlabel value="City"/>
                   <apex:inputtext value="{!city}"/> 
                 </apex:pageBlockSectionItem>
                  
                 <apex:commandButton action="{! search}" value="Search" reRender="mymap" />  
                  
                 <!-- pass slider values -->
                 <apex:inputhidden value="{!size_low}" id="size_low" /> 
                 <apex:inputhidden value="{!size_high}" id="size_high" />
                 <!-- pass address of clicked marker -->
                 <apex:inputhidden value="{!addressParameter}" id="addressParameter" /> 
                  
               </apex:pageBlockSection>
               
               <apex:pageBlockSection title="Filter Results" columns="3">
                     
                     <apex:pageBlockSectionItem >
                        <apex:outputLabel value="Size"/>
                        <!-- This is where our slider will be -->
                        <div id="slider-range" style="font-size: 90%; margin-top: 0.5em;"></div>
                        <div id="amountValue" style="text-align: center;"></div>
                     </apex:pageBlockSectionItem>
                     
                     <apex:pageBlockSectionItem >
                        <apex:outputlabel value="Begining Year"/>
                        <apex:selectList value="{!b_year}" multiselect="false" required="true" size="1">
                           <apex:selectOptions value="{!years}"/>
                        </apex:selectList>
                     </apex:pageBlockSectionItem>
                     
                     <apex:pageBlockSectionItem >
                        <apex:outputlabel value="End Year"/>
                        <apex:selectList value="{!e_year}" multiselect="false" required="true" size="1">
                           <apex:selectOptions value="{!years}"/>
                        </apex:selectList>
                     </apex:pageBlockSectionItem>
                     
                     <apex:pageBlockSectionItem >
                        <apex:outputlabel value="Type"/>
                        <apex:selectCheckboxes value="{!type}">
                           <apex:selectOptions value="{!items}"/>
                        </apex:selectCheckboxes>
                     </apex:pageBlockSectionItem>
                     
               </apex:pageBlockSection>
               
          <apex:pageBlockSection columns="2">
                <apex:outputpanel id="mymap">
                  <div id="map_canvas"></div>
 
                  <script type="text/javascript">
                     
                     var myOptions = {
                            zoom:15,
                            mapTypeControl: false,
                            mapTypeId: google.maps.MapTypeId.ROADMAP
                         };
                     var geocoder = new google.maps.Geocoder();
                     
                    //create map
                     var map = new google.maps.Map(document.getElementById("map_canvas"),myOptions);
                 
                    function putmarker(map,address,geocoder){
                       geocoder.geocode( { address: address}, function(results, status) {
                        if (status == google.maps.GeocoderStatus.OK && results.length) {
                          if (status != google.maps.GeocoderStatus.ZERO_RESULTS) {
      
                              //center map
                                map.setCenter(results[0].geometry.location);
        
                             //create marker
                                var marker = new google.maps.Marker({
                                  position: results[0].geometry.location,
                                  map: map,
                                  title:address
                                });
                  
                             //add click listener on marker
                               google.maps.event.addListener(marker, 'click', function() {
                                  //change addressParameter on click
                                    document.getElementById('{!$Component.page.form.block.values.addressParameter}').value=marker.title;
                                    refreshData(); //to rerender rightpane containing sales data of a location(invokes action function)
                         
                               });
                           }
                        }
                       })
                     }
                  </script>
                
                  <apex:repeat value="{! salesAddress}" var="address">
                      <script> putmarker(map,"{!address}",geocoder);</script>
                  </apex:repeat>
               
                </apex:outputpanel>
                
                <apex:dataList value="{! Sales}" var="sale" id="dataView">
                    <!-- extract Record Id(15 character long from begining) to make link and Text to display(from 20th character to end of string) --> 
                    <!-- Instance is auto corrected by force.com if it is not ap1 -->
                        <apex:outputLink value="{!'https://ap1.salesforce.com/'+left(sale,15)}" target="_blank">
                            {!MID(sale,20,len(sale)-19)}
                        </apex:outputLink>
                 </apex:dataList>
                
          </apex:pageBlockSection>
          
         </apex:pageBlock>
               <!-- Rerender Right pane on marker click -->
               <apex:actionFunction name="refreshData" action="{!refreshData}" rerender="dataView"/>
          
     </apex:form>
                 
</apex:page>
Controller :