Advertisement
Guest User

importJSON

a guest
Jan 20th, 2016
2,712
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /*====================================================================================================================================*
  2.   ImportJSON by Trevor Lohrbeer (@FastFedora)
  3.   ====================================================================================================================================
  4.   Version:      1.2.1
  5.   Project Page: http://blog.fastfedora.com/projects/import-json
  6.   Copyright:    (c) 2012-2013 by Trevor Lohrbeer
  7.   License:      GNU General Public License, version 3 (GPL-3.0)
  8.                 http://www.opensource.org/licenses/gpl-3.0.html
  9.   ------------------------------------------------------------------------------------------------------------------------------------
  10.   A library for importing JSON feeds into Google spreadsheets. Functions include:
  11.  
  12.      ImportJSON            For use by end users to import a JSON feed from a URL
  13.      ImportJSONViaPost     For use by end users to import a JSON feed from a URL using POST parameters
  14.      ImportJSONAdvanced    For use by script developers to easily extend the functionality of this library
  15.  
  16.   Future enhancements may include:
  17.  
  18.    - Support for a real XPath like syntax similar to ImportXML for the query parameter
  19.    - Support for OAuth authenticated APIs (see AddOAuthService__ function for failed experiment)
  20.  
  21.   Or feel free to write these and add on to the library yourself!
  22.   ------------------------------------------------------------------------------------------------------------------------------------
  23.   Changelog:
  24.  
  25.   1.2.1  Fixed a bug with how nested arrays are handled. The rowIndex counter wasn't incrementing properly when parsing.
  26.   1.2.0  Added ImportJSONViaPost and support for fetchOptions to ImportJSONAdvanced
  27.   1.1.1  Added a version number using Google Scripts Versioning so other developers can use the library
  28.   1.1    Added support for the noHeaders option
  29.   1.0    Initial release
  30.  *====================================================================================================================================*/
  31. /**
  32.  * Imports a JSON feed and returns the results to be inserted into a Google Spreadsheet. The JSON feed is flattened to create
  33.  * a two-dimensional array. The first row contains the headers, with each column header indicating the path to that data in
  34.  * the JSON feed. The remaining rows contain the data.
  35.  *
  36.  * By default, data gets transformed so it looks more like a normal data import. Specifically:
  37.  *
  38.  *   - Data from parent JSON elements gets inherited to their child elements, so rows representing child elements contain the values
  39.  *      of the rows representing their parent elements.
  40.  *   - Values longer than 256 characters get truncated.
  41.  *   - Headers have slashes converted to spaces, common prefixes removed and the resulting text converted to title case.
  42.  *
  43.  * To change this behavior, pass in one of these values in the options parameter:
  44.  *
  45.  *    noInherit:     Don't inherit values from parent elements
  46.  *    noTruncate:    Don't truncate values
  47.  *    rawHeaders:    Don't prettify headers
  48.  *    noHeaders:     Don't include headers, only the data
  49.  *    debugLocation: Prepend each value with the row & column it belongs in
  50.  *
  51.  * For example:
  52.  *
  53.  *   =ImportJSON("http://gdata.youtube.com/feeds/api/standardfeeds/most_popular?v=2&alt=json", "/feed/entry/title,/feed/entry/content",
  54.  *               "noInherit,noTruncate,rawHeaders")
  55.  *
  56.  * @param {url}          the URL to a public JSON feed
  57.  * @param {query}        a comma-separated list of paths to import. Any path starting with one of these paths gets imported.
  58.  * @param {parseOptions} a comma-separated list of options that alter processing of the data
  59.  *
  60.  * @return a two-dimensional array containing the data, with the first row containing headers
  61.  **/
  62. function ImportJSON(url, query, parseOptions) {
  63.   return ImportJSONAdvanced(url, null, query, parseOptions, includeXPath_, defaultTransform_); //
  64. }
  65.  
  66. /**
  67.  * Imports a JSON feed via a POST request and returns the results to be inserted into a Google Spreadsheet. The JSON feed is
  68.  * flattened to create a two-dimensional array. The first row contains the headers, with each column header indicating the path to
  69.  * that data in the JSON feed. The remaining rows contain the data.
  70.  *
  71.  * To retrieve the JSON, a POST request is sent to the URL and the payload is passed as the content of the request using the content
  72.  * type "application/x-www-form-urlencoded". If the fetchOptions define a value for "method", "payload" or "contentType", these
  73.  * values will take precedent. For example, advanced users can use this to make this function pass XML as the payload using a GET
  74.  * request and a content type of "application/xml; charset=utf-8". For more information on the available fetch options, see
  75.  * https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app . At this time the "headers" option is not supported.
  76.  *
  77.  * By default, the returned data gets transformed so it looks more like a normal data import. Specifically:
  78.  *
  79.  *   - Data from parent JSON elements gets inherited to their child elements, so rows representing child elements contain the values
  80.  *     of the rows representing their parent elements.
  81.  *   - Values longer than 256 characters get truncated.
  82.  *   - Headers have slashes converted to spaces, common prefixes removed and the resulting text converted to title case.
  83.  *
  84.  * To change this behavior, pass in one of these values in the options parameter:
  85.  *
  86.  *    noInherit:     Don't inherit values from parent elements
  87.  *    noTruncate:    Don't truncate values
  88.  *    rawHeaders:    Don't prettify headers
  89.  *    noHeaders:     Don't include headers, only the data
  90.  *    debugLocation: Prepend each value with the row & column it belongs in
  91.  *
  92.  * For example:
  93.  *
  94.  *   =ImportJSON("http://gdata.youtube.com/feeds/api/standardfeeds/most_popular?v=2&alt=json", "user=bob&apikey=xxxx",
  95.  *               "validateHttpsCertificates=false", "/feed/entry/title,/feed/entry/content", "noInherit,noTruncate,rawHeaders")
  96.  *
  97.  * @param {url}          the URL to a public JSON feed
  98.  * @param {payload}      the content to pass with the POST request; usually a URL encoded list of parameters separated by ampersands
  99.  * @param {fetchOptions} a comma-separated list of options used to retrieve the JSON feed from the URL
  100.  * @param {query}        a comma-separated list of paths to import. Any path starting with one of these paths gets imported.
  101.  * @param {parseOptions} a comma-separated list of options that alter processing of the data
  102.  *
  103.  * @return a two-dimensional array containing the data, with the first row containing headers
  104.  **/
  105. function ImportJSONViaPost(url, payload, fetchOptions, query, parseOptions) {
  106.   var postOptions = parseToObject_(fetchOptions);
  107.  
  108.   if (postOptions["method"] == null) {
  109.     postOptions["method"] = "POST";
  110.   }
  111.  
  112.   if (postOptions["payload"] == null) {
  113.     postOptions["payload"] = payload;
  114.   }
  115.  
  116.   if (postOptions["contentType"] == null) {
  117.     postOptions["contentType"] = "application/x-www-form-urlencoded";
  118.   }
  119.  
  120.   convertToBool_(postOptions, "validateHttpsCertificates");
  121.   convertToBool_(postOptions, "useIntranet");
  122.   convertToBool_(postOptions, "followRedirects");
  123.   convertToBool_(postOptions, "muteHttpExceptions");
  124.  
  125.   return ImportJSONAdvanced(url, postOptions, query, parseOptions, includeXPath_, defaultTransform_);
  126. }
  127.  
  128. /**
  129.  * An advanced version of ImportJSON designed to be easily extended by a script. This version cannot be called from within a
  130.  * spreadsheet.
  131.  *
  132.  * Imports a JSON feed and returns the results to be inserted into a Google Spreadsheet. The JSON feed is flattened to create
  133.  * a two-dimensional array. The first row contains the headers, with each column header indicating the path to that data in
  134.  * the JSON feed. The remaining rows contain the data.
  135.  *
  136.  * The fetchOptions can be used to change how the JSON feed is retrieved. For instance, the "method" and "payload" options can be
  137.  * set to pass a POST request with post parameters. For more information on the available parameters, see
  138.  * https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app .
  139.  *
  140.  * Use the include and transformation functions to determine what to include in the import and how to transform the data after it is
  141.  * imported.
  142.  *
  143.  * For example:
  144.  *
  145.  *   ImportJSON("http://gdata.youtube.com/feeds/api/standardfeeds/most_popular?v=2&alt=json",
  146.  *              new Object() { "method" : "post", "payload" : "user=bob&apikey=xxxx" },
  147.  *              "/feed/entry",
  148.  *              "",
  149.  *              function (query, path) { return path.indexOf(query) == 0; },
  150.  *              function (data, row, column) { data[row][column] = data[row][column].toString().substr(0, 100); } )
  151.  *
  152.  * In this example, the import function checks to see if the path to the data being imported starts with the query. The transform
  153.  * function takes the data and truncates it. For more robust versions of these functions, see the internal code of this library.
  154.  *
  155.  * @param {url}           the URL to a public JSON feed
  156.  * @param {fetchOptions}  an object whose properties are options used to retrieve the JSON feed from the URL
  157.  * @param {query}         the query passed to the include function
  158.  * @param {parseOptions}  a comma-separated list of options that may alter processing of the data
  159.  * @param {includeFunc}   a function with the signature func(query, path, options) that returns true if the data element at the given path
  160.  *                        should be included or false otherwise.
  161.  * @param {transformFunc} a function with the signature func(data, row, column, options) where data is a 2-dimensional array of the data
  162.  *                        and row & column are the current row and column being processed. Any return value is ignored. Note that row 0
  163.  *                        contains the headers for the data, so test for row==0 to process headers only.
  164.  *
  165.  * @return a two-dimensional array containing the data, with the first row containing headers
  166.  **/
  167. function ImportJSONAdvanced(url, fetchOptions, query, parseOptions, includeFunc, transformFunc) {
  168.   var jsondata = UrlFetchApp.fetch(url, fetchOptions);
  169.   var object   = JSON.parse(jsondata.getContentText());
  170.  
  171.   return parseJSONObject_(object, query, parseOptions, includeFunc, transformFunc);
  172. }
  173.  
  174. /**
  175.  * Encodes the given value to use within a URL.
  176.  *
  177.  * @param {value} the value to be encoded
  178.  *
  179.  * @return the value encoded using URL percent-encoding
  180.  */
  181. function URLEncode(value) {
  182.   return encodeURIComponent(value.toString());  
  183. }
  184.  
  185. /**
  186.  * Adds an oAuth service using the given name and the list of properties.
  187.  *
  188.  * @note This method is an experiment in trying to figure out how to add an oAuth service without having to specify it on each
  189.  *       ImportJSON call. The idea was to call this method in the first cell of a spreadsheet, and then use ImportJSON in other
  190.  *       cells. This didn't work, but leaving this in here for further experimentation later.
  191.  *
  192.  *       The test I did was to add the following into the A1:
  193.  *  
  194.  *           =AddOAuthService("twitter", "https://api.twitter.com/oauth/access_token",
  195.  *                            "https://api.twitter.com/oauth/request_token", "https://api.twitter.com/oauth/authorize",
  196.  *                            "<my consumer key>", "<my consumer secret>", "", "")
  197.  *
  198.  *       Information on obtaining a consumer key & secret for Twitter can be found at https://dev.twitter.com/docs/auth/using-oauth
  199.  *
  200.  *       Then I added the following into A2:
  201.  *
  202.  *           =ImportJSONViaPost("https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=fastfedora&count=2", "",
  203.  *                              "oAuthServiceName=twitter,oAuthUseToken=always", "/", "")
  204.  *
  205.  *       I received an error that the "oAuthServiceName" was not a valid value. [twl 18.Apr.13]
  206.  */
  207. function AddOAuthService__(name, accessTokenUrl, requestTokenUrl, authorizationUrl, consumerKey, consumerSecret, method, paramLocation) {
  208.   var oAuthConfig = UrlFetchApp.addOAuthService(name);
  209.  
  210.   if (accessTokenUrl != null && accessTokenUrl.length > 0) {
  211.     oAuthConfig.setAccessTokenUrl(accessTokenUrl);
  212.   }
  213.  
  214.   if (requestTokenUrl != null && requestTokenUrl.length > 0) {
  215.     oAuthConfig.setRequestTokenUrl(requestTokenUrl);
  216.   }
  217.  
  218.   if (authorizationUrl != null && authorizationUrl.length > 0) {
  219.     oAuthConfig.setAuthorizationUrl(authorizationUrl);
  220.   }
  221.  
  222.   if (consumerKey != null && consumerKey.length > 0) {
  223.     oAuthConfig.setConsumerKey(consumerKey);
  224.   }
  225.  
  226.   if (consumerSecret != null && consumerSecret.length > 0) {
  227.     oAuthConfig.setConsumerSecret(consumerSecret);
  228.   }
  229.  
  230.   if (method != null && method.length > 0) {
  231.     oAuthConfig.setMethod(method);
  232.   }
  233.  
  234.   if (paramLocation != null && paramLocation.length > 0) {
  235.     oAuthConfig.setParamLocation(paramLocation);
  236.   }
  237. }
  238.  
  239. /**
  240.  * Parses a JSON object and returns a two-dimensional array containing the data of that object.
  241.  */
  242. function parseJSONObject_(object, query, options, includeFunc, transformFunc) {
  243.   var headers = new Array();
  244.   var data    = new Array();
  245.  
  246.   if (query && !Array.isArray(query) && query.toString().indexOf(",") != -1) {
  247.     query = query.toString().split(",");
  248.   }
  249.  
  250.   if (options) {
  251.     options = options.toString().split(",");
  252.   }
  253.    
  254.   parseData_(headers, data, "", {rowIndex: 1}, object, query, options, includeFunc);
  255.   parseHeaders_(headers, data);
  256.   transformData_(data, options, transformFunc);
  257.  
  258.   return hasOption_(options, "noHeaders") ? (data.length > 1 ? data.slice(1) : new Array()) : data;
  259. }
  260.  
  261. /**
  262.  * Parses the data contained within the given value and inserts it into the data two-dimensional array starting at the rowIndex.
  263.  * If the data is to be inserted into a new column, a new header is added to the headers array. The value can be an object,
  264.  * array or scalar value.
  265.  *
  266.  * If the value is an object, it's properties are iterated through and passed back into this function with the name of each
  267.  * property extending the path. For instance, if the object contains the property "entry" and the path passed in was "/feed",
  268.  * this function is called with the value of the entry property and the path "/feed/entry".
  269.  *
  270.  * If the value is an array containing other arrays or objects, each element in the array is passed into this function with
  271.  * the rowIndex incremeneted for each element.
  272.  *
  273.  * If the value is an array containing only scalar values, those values are joined together and inserted into the data array as
  274.  * a single value.
  275.  *
  276.  * If the value is a scalar, the value is inserted directly into the data array.
  277.  */
  278. function parseData_(headers, data, path, state, value, query, options, includeFunc) {
  279.   var dataInserted = false;
  280.  
  281.   if (Array.isArray(value) && isObjectArray_(value)) {
  282.     for (var i = 0; i < value.length; i++) {
  283.       if (parseData_(headers, data, path, state, value[i], query, options, includeFunc)) {
  284.         dataInserted = true;
  285.  
  286.         if (i > 0 && data[state.rowIndex]) {
  287.           state.rowIndex++;
  288.         }
  289.       }
  290.     }
  291.   } else if (isObject_(value)) {
  292.     for (key in value) {
  293.       if (parseData_(headers, data, path + "/" + key, state, value[key], query, options, includeFunc)) {
  294.         dataInserted = true;
  295.       }
  296.     }
  297.   } else if (!includeFunc || includeFunc(query, path, options)) {
  298.     // Handle arrays containing only scalar values
  299.     if (Array.isArray(value)) {
  300.       value = value.join();
  301.     }
  302.    
  303.     // Insert new row if one doesn't already exist
  304.     if (!data[state.rowIndex]) {
  305.       data[state.rowIndex] = new Array();
  306.     }
  307.    
  308.     // Add a new header if one doesn't exist
  309.     if (!headers[path] && headers[path] != 0) {
  310.       headers[path] = Object.keys(headers).length;
  311.     }
  312.    
  313.     // Insert the data
  314.     data[state.rowIndex][headers[path]] = value;
  315.     dataInserted = true;
  316.   }
  317.  
  318.   return dataInserted;
  319. }
  320.  
  321. /**
  322.  * Parses the headers array and inserts it into the first row of the data array.
  323.  */
  324. function parseHeaders_(headers, data) {
  325.   data[0] = new Array();
  326.  
  327.   for (key in headers) {
  328.     data[0][headers[key]] = key;
  329.   }
  330. }
  331.  
  332. /**
  333.  * Applies the transform function for each element in the data array, going through each column of each row.
  334.  */
  335. function transformData_(data, options, transformFunc) {
  336.   for (var i = 0; i < data.length; i++) {
  337.     for (var j = 0; j < data[i].length; j++) {
  338.       transformFunc(data, i, j, options);
  339.     }
  340.   }
  341. }
  342.  
  343. /**
  344.  * Returns true if the given test value is an object; false otherwise.
  345.  */
  346. function isObject_(test) {
  347.   return Object.prototype.toString.call(test) === '[object Object]';
  348. }
  349.  
  350. /**
  351.  * Returns true if the given test value is an array containing at least one object; false otherwise.
  352.  */
  353. function isObjectArray_(test) {
  354.   for (var i = 0; i < test.length; i++) {
  355.     if (isObject_(test[i])) {
  356.       return true;
  357.     }
  358.   }  
  359.  
  360.   return false;
  361. }
  362.  
  363. /**
  364.  * Returns true if the given query applies to the given path.
  365.  */
  366. function includeXPath_(query, path, options) {
  367.   if (!query) {
  368.     return true;
  369.   } else if (Array.isArray(query)) {
  370.     for (var i = 0; i < query.length; i++) {
  371.       if (applyXPathRule_(query[i], path, options)) {
  372.         return true;
  373.       }
  374.     }  
  375.   } else {
  376.     return applyXPathRule_(query, path, options);
  377.   }
  378.  
  379.   return false;
  380. };
  381.  
  382. /**
  383.  * Returns true if the rule applies to the given path.
  384.  */
  385. function applyXPathRule_(rule, path, options) {
  386.   return path.indexOf(rule) == 0;
  387. }
  388.  
  389. /**
  390.  * By default, this function transforms the value at the given row & column so it looks more like a normal data import. Specifically:
  391.  *
  392.  *   - Data from parent JSON elements gets inherited to their child elements, so rows representing child elements contain the values
  393.  *     of the rows representing their parent elements.
  394.  *   - Values longer than 256 characters get truncated.
  395.  *   - Values in row 0 (headers) have slashes converted to spaces, common prefixes removed and the resulting text converted to title
  396. *      case.
  397.  *
  398.  * To change this behavior, pass in one of these values in the options parameter:
  399.  *
  400.  *    noInherit:     Don't inherit values from parent elements
  401.  *    noTruncate:    Don't truncate values
  402.  *    rawHeaders:    Don't prettify headers
  403.  *    debugLocation: Prepend each value with the row & column it belongs in
  404.  */
  405. function defaultTransform_(data, row, column, options) {
  406.   if (!data[row][column]) {
  407.     if (row < 2 || hasOption_(options, "noInherit")) {
  408.       data[row][column] = "";
  409.     } else {
  410.       data[row][column] = data[row-1][column];
  411.     }
  412.   }
  413.  
  414.   if (!hasOption_(options, "rawHeaders") && row == 0) {
  415.     if (column == 0 && data[row].length > 1) {
  416.       removeCommonPrefixes_(data, row);  
  417.     }
  418.    
  419.     data[row][column] = toTitleCase_(data[row][column].toString().replace(/[\/\_]/g, " "));
  420.   }
  421.  
  422.   if (!hasOption_(options, "noTruncate") && data[row][column]) {
  423.     data[row][column] = data[row][column].toString().substr(0, 256);
  424.   }
  425.  
  426.   if (hasOption_(options, "debugLocation")) {
  427.     data[row][column] = "[" + row + "," + column + "]" + data[row][column];
  428.   }
  429. }
  430.  
  431. /**
  432.  * If all the values in the given row share the same prefix, remove that prefix.
  433.  */
  434. function removeCommonPrefixes_(data, row) {
  435.   var matchIndex = data[row][0].length;
  436.  
  437.   for (var i = 1; i < data[row].length; i++) {
  438.     matchIndex = findEqualityEndpoint_(data[row][i-1], data[row][i], matchIndex);
  439.  
  440.     if (matchIndex == 0) {
  441.       return;
  442.     }
  443.   }
  444.  
  445.   for (var i = 0; i < data[row].length; i++) {
  446.     data[row][i] = data[row][i].substring(matchIndex, data[row][i].length);
  447.   }
  448. }
  449.  
  450. /**
  451.  * Locates the index where the two strings values stop being equal, stopping automatically at the stopAt index.
  452.  */
  453. function findEqualityEndpoint_(string1, string2, stopAt) {
  454.   if (!string1 || !string2) {
  455.     return -1;
  456.   }
  457.  
  458.   var maxEndpoint = Math.min(stopAt, string1.length, string2.length);
  459.  
  460.   for (var i = 0; i < maxEndpoint; i++) {
  461.     if (string1.charAt(i) != string2.charAt(i)) {
  462.       return i;
  463.     }
  464.   }
  465.  
  466.   return maxEndpoint;
  467. }
  468.  
  469.  
  470. /**
  471.  * Converts the text to title case.
  472.  */
  473. function toTitleCase_(text) {
  474.   if (text == null) {
  475.     return null;
  476.   }
  477.  
  478.   return text.replace(/\w\S*/g, function(word) { return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase(); });
  479. }
  480.  
  481. /**
  482.  * Returns true if the given set of options contains the given option.
  483.  */
  484. function hasOption_(options, option) {
  485.   return options && options.indexOf(option) >= 0;
  486. }
  487.  
  488. /**
  489.  * Parses the given string into an object, trimming any leading or trailing spaces from the keys.
  490.  */
  491. function parseToObject_(text) {
  492.   var map     = new Object();
  493.   var entries = (text != null && text.trim().length > 0) ? text.toString().split(",") : new Array();
  494.  
  495.   for (var i = 0; i < entries.length; i++) {
  496.     addToMap_(map, entries[i]);  
  497.   }
  498.  
  499.   return map;
  500. }
  501.  
  502. /**
  503.  * Parses the given entry and adds it to the given map, trimming any leading or trailing spaces from the key.
  504.  */
  505. function addToMap_(map, entry) {
  506.   var equalsIndex = entry.indexOf("=");  
  507.   var key         = (equalsIndex != -1) ? entry.substring(0, equalsIndex) : entry;
  508.   var value       = (key.length + 1 < entry.length) ? entry.substring(key.length + 1) : "";
  509.  
  510.   map[key.trim()] = value;
  511. }
  512.  
  513. /**
  514.  * Returns the given value as a boolean.
  515.  */
  516. function toBool_(value) {
  517.   return value == null ? false : (value.toString().toLowerCase() == "true" ? true : false);
  518. }
  519.  
  520. /**
  521.  * Converts the value for the given key in the given map to a bool.
  522.  */
  523. function convertToBool_(map, key) {
  524.   if (map[key] != null) {
  525.     map[key] = toBool_(map[key]);
  526.   }  
  527. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement