1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "google_apis/drive/gdata_wapi_parser.h"
6
7#include <algorithm>
8#include <string>
9
10#include "base/basictypes.h"
11#include "base/files/file_path.h"
12#include "base/json/json_value_converter.h"
13#include "base/memory/scoped_ptr.h"
14#include "base/strings/string_number_conversions.h"
15#include "base/strings/string_piece.h"
16#include "base/strings/string_util.h"
17#include "base/strings/utf_string_conversions.h"
18#include "base/values.h"
19#include "google_apis/drive/time_util.h"
20
21using base::Value;
22using base::DictionaryValue;
23using base::ListValue;
24
25namespace google_apis {
26
27namespace {
28
29// Term values for kSchemeKind category:
30const char kTermPrefix[] = "http://schemas.google.com/docs/2007#";
31
32// Node names.
33const char kEntryNode[] = "entry";
34
35// Field names.
36const char kAuthorField[] = "author";
37const char kCategoryField[] = "category";
38const char kChangestampField[] = "docs$changestamp.value";
39const char kContentField[] = "content";
40const char kDeletedField[] = "gd$deleted";
41const char kETagField[] = "gd$etag";
42const char kEmailField[] = "email.$t";
43const char kEntryField[] = "entry";
44const char kFeedField[] = "feed";
45const char kFeedLinkField[] = "gd$feedLink";
46const char kFileNameField[] = "docs$filename.$t";
47const char kHrefField[] = "href";
48const char kIDField[] = "id.$t";
49const char kItemsPerPageField[] = "openSearch$itemsPerPage.$t";
50const char kLabelField[] = "label";
51const char kLargestChangestampField[] = "docs$largestChangestamp.value";
52const char kLastViewedField[] = "gd$lastViewed.$t";
53const char kLinkField[] = "link";
54const char kMD5Field[] = "docs$md5Checksum.$t";
55const char kNameField[] = "name.$t";
56const char kPublishedField[] = "published.$t";
57const char kRelField[] = "rel";
58const char kRemovedField[] = "docs$removed";
59const char kResourceIdField[] = "gd$resourceId.$t";
60const char kSchemeField[] = "scheme";
61const char kSizeField[] = "docs$size.$t";
62const char kSrcField[] = "src";
63const char kStartIndexField[] = "openSearch$startIndex.$t";
64const char kSuggestedFileNameField[] = "docs$suggestedFilename.$t";
65const char kTermField[] = "term";
66const char kTitleField[] = "title";
67const char kTitleTField[] = "title.$t";
68const char kTypeField[] = "type";
69const char kUpdatedField[] = "updated.$t";
70
71// Link Prefixes
72const char kOpenWithPrefix[] = "http://schemas.google.com/docs/2007#open-with-";
73const size_t kOpenWithPrefixSize = arraysize(kOpenWithPrefix) - 1;
74
75struct EntryKindMap {
76  DriveEntryKind kind;
77  const char* entry;
78  const char* extension;
79};
80
81const EntryKindMap kEntryKindMap[] = {
82    { ENTRY_KIND_UNKNOWN,      "unknown",      NULL},
83    { ENTRY_KIND_ITEM,         "item",         NULL},
84    { ENTRY_KIND_DOCUMENT,     "document",     ".gdoc"},
85    { ENTRY_KIND_SPREADSHEET,  "spreadsheet",  ".gsheet"},
86    { ENTRY_KIND_PRESENTATION, "presentation", ".gslides" },
87    { ENTRY_KIND_DRAWING,      "drawing",      ".gdraw"},
88    { ENTRY_KIND_TABLE,        "table",        ".gtable"},
89    { ENTRY_KIND_FORM,         "form",         ".gform"},
90    { ENTRY_KIND_EXTERNAL_APP, "externalapp",  ".glink"},
91    { ENTRY_KIND_SITE,         "site",         NULL},
92    { ENTRY_KIND_FOLDER,       "folder",       NULL},
93    { ENTRY_KIND_FILE,         "file",         NULL},
94    { ENTRY_KIND_PDF,          "pdf",          NULL},
95};
96COMPILE_ASSERT(arraysize(kEntryKindMap) == ENTRY_KIND_MAX_VALUE,
97               EntryKindMap_and_DriveEntryKind_are_not_in_sync);
98
99struct LinkTypeMap {
100  Link::LinkType type;
101  const char* rel;
102};
103
104const LinkTypeMap kLinkTypeMap[] = {
105    { Link::LINK_SELF,
106      "self" },
107    { Link::LINK_NEXT,
108      "next" },
109    { Link::LINK_PARENT,
110      "http://schemas.google.com/docs/2007#parent" },
111    { Link::LINK_ALTERNATE,
112      "alternate"},
113    { Link::LINK_EDIT,
114      "edit" },
115    { Link::LINK_EDIT_MEDIA,
116      "edit-media" },
117    { Link::LINK_ALT_EDIT_MEDIA,
118      "http://schemas.google.com/docs/2007#alt-edit-media" },
119    { Link::LINK_ALT_POST,
120      "http://schemas.google.com/docs/2007#alt-post" },
121    { Link::LINK_FEED,
122      "http://schemas.google.com/g/2005#feed"},
123    { Link::LINK_POST,
124      "http://schemas.google.com/g/2005#post"},
125    { Link::LINK_BATCH,
126      "http://schemas.google.com/g/2005#batch"},
127    { Link::LINK_THUMBNAIL,
128      "http://schemas.google.com/docs/2007/thumbnail"},
129    { Link::LINK_RESUMABLE_EDIT_MEDIA,
130      "http://schemas.google.com/g/2005#resumable-edit-media"},
131    { Link::LINK_RESUMABLE_CREATE_MEDIA,
132      "http://schemas.google.com/g/2005#resumable-create-media"},
133    { Link::LINK_TABLES_FEED,
134      "http://schemas.google.com/spreadsheets/2006#tablesfeed"},
135    { Link::LINK_WORKSHEET_FEED,
136      "http://schemas.google.com/spreadsheets/2006#worksheetsfeed"},
137    { Link::LINK_EMBED,
138      "http://schemas.google.com/docs/2007#embed"},
139    { Link::LINK_PRODUCT,
140      "http://schemas.google.com/docs/2007#product"},
141    { Link::LINK_ICON,
142      "http://schemas.google.com/docs/2007#icon"},
143    { Link::LINK_SHARE,
144      "http://schemas.google.com/docs/2007#share"},
145};
146
147struct ResourceLinkTypeMap {
148  ResourceLink::ResourceLinkType type;
149  const char* rel;
150};
151
152const ResourceLinkTypeMap kFeedLinkTypeMap[] = {
153    { ResourceLink::FEED_LINK_ACL,
154      "http://schemas.google.com/acl/2007#accessControlList" },
155    { ResourceLink::FEED_LINK_REVISIONS,
156      "http://schemas.google.com/docs/2007/revisions" },
157};
158
159struct CategoryTypeMap {
160  Category::CategoryType type;
161  const char* scheme;
162};
163
164const CategoryTypeMap kCategoryTypeMap[] = {
165    { Category::CATEGORY_KIND, "http://schemas.google.com/g/2005#kind" },
166    { Category::CATEGORY_LABEL, "http://schemas.google.com/g/2005/labels" },
167};
168
169// Converts |url_string| to |result|.  Always returns true to be used
170// for JSONValueConverter::RegisterCustomField method.
171// TODO(mukai): make it return false in case of invalid |url_string|.
172bool GetGURLFromString(const base::StringPiece& url_string, GURL* result) {
173  *result = GURL(url_string.as_string());
174  return true;
175}
176
177}  // namespace
178
179////////////////////////////////////////////////////////////////////////////////
180// Author implementation
181
182Author::Author() {
183}
184
185// static
186void Author::RegisterJSONConverter(
187    base::JSONValueConverter<Author>* converter) {
188  converter->RegisterStringField(kNameField, &Author::name_);
189  converter->RegisterStringField(kEmailField, &Author::email_);
190}
191
192////////////////////////////////////////////////////////////////////////////////
193// Link implementation
194
195Link::Link() : type_(Link::LINK_UNKNOWN) {
196}
197
198Link::~Link() {
199}
200
201// static
202bool Link::GetAppID(const base::StringPiece& rel, std::string* app_id) {
203  DCHECK(app_id);
204  // Fast return path if the link clearly isn't an OPEN_WITH link.
205  if (rel.size() < kOpenWithPrefixSize) {
206    app_id->clear();
207    return true;
208  }
209
210  const std::string kOpenWithPrefixStr(kOpenWithPrefix);
211  if (StartsWithASCII(rel.as_string(), kOpenWithPrefixStr, false)) {
212    *app_id = rel.as_string().substr(kOpenWithPrefixStr.size());
213    return true;
214  }
215
216  app_id->clear();
217  return true;
218}
219
220// static.
221bool Link::GetLinkType(const base::StringPiece& rel, Link::LinkType* type) {
222  DCHECK(type);
223  for (size_t i = 0; i < arraysize(kLinkTypeMap); i++) {
224    if (rel == kLinkTypeMap[i].rel) {
225      *type = kLinkTypeMap[i].type;
226      return true;
227    }
228  }
229
230  // OPEN_WITH links have extra information at the end of the rel that is unique
231  // for each one, so we can't just check the usual map. This check is slightly
232  // redundant to provide a quick skip if it's obviously not an OPEN_WITH url.
233  if (rel.size() >= kOpenWithPrefixSize &&
234      StartsWithASCII(rel.as_string(), kOpenWithPrefix, false)) {
235    *type = LINK_OPEN_WITH;
236    return true;
237  }
238
239  // Let unknown link types through, just report it; if the link type is needed
240  // in the future, add it into LinkType and kLinkTypeMap.
241  DVLOG(1) << "Ignoring unknown link type for rel " << rel;
242  *type = LINK_UNKNOWN;
243  return true;
244}
245
246// static
247void Link::RegisterJSONConverter(base::JSONValueConverter<Link>* converter) {
248  converter->RegisterCustomField<Link::LinkType>(kRelField,
249                                                 &Link::type_,
250                                                 &Link::GetLinkType);
251  // We have to register kRelField twice because we extract two different pieces
252  // of data from the same rel field.
253  converter->RegisterCustomField<std::string>(kRelField,
254                                              &Link::app_id_,
255                                              &Link::GetAppID);
256  converter->RegisterCustomField(kHrefField, &Link::href_, &GetGURLFromString);
257  converter->RegisterStringField(kTitleField, &Link::title_);
258  converter->RegisterStringField(kTypeField, &Link::mime_type_);
259}
260
261////////////////////////////////////////////////////////////////////////////////
262// ResourceLink implementation
263
264ResourceLink::ResourceLink() : type_(ResourceLink::FEED_LINK_UNKNOWN) {
265}
266
267// static.
268bool ResourceLink::GetFeedLinkType(
269    const base::StringPiece& rel, ResourceLink::ResourceLinkType* result) {
270  for (size_t i = 0; i < arraysize(kFeedLinkTypeMap); i++) {
271    if (rel == kFeedLinkTypeMap[i].rel) {
272      *result = kFeedLinkTypeMap[i].type;
273      return true;
274    }
275  }
276  DVLOG(1) << "Unknown feed link type for rel " << rel;
277  return false;
278}
279
280// static
281void ResourceLink::RegisterJSONConverter(
282    base::JSONValueConverter<ResourceLink>* converter) {
283  converter->RegisterCustomField<ResourceLink::ResourceLinkType>(
284      kRelField, &ResourceLink::type_, &ResourceLink::GetFeedLinkType);
285  converter->RegisterCustomField(
286      kHrefField, &ResourceLink::href_, &GetGURLFromString);
287}
288
289////////////////////////////////////////////////////////////////////////////////
290// Category implementation
291
292Category::Category() : type_(CATEGORY_UNKNOWN) {
293}
294
295// Converts category.scheme into CategoryType enum.
296bool Category::GetCategoryTypeFromScheme(
297    const base::StringPiece& scheme, Category::CategoryType* result) {
298  for (size_t i = 0; i < arraysize(kCategoryTypeMap); i++) {
299    if (scheme == kCategoryTypeMap[i].scheme) {
300      *result = kCategoryTypeMap[i].type;
301      return true;
302    }
303  }
304  DVLOG(1) << "Unknown feed link type for scheme " << scheme;
305  return false;
306}
307
308// static
309void Category::RegisterJSONConverter(
310    base::JSONValueConverter<Category>* converter) {
311  converter->RegisterStringField(kLabelField, &Category::label_);
312  converter->RegisterCustomField<Category::CategoryType>(
313      kSchemeField, &Category::type_, &Category::GetCategoryTypeFromScheme);
314  converter->RegisterStringField(kTermField, &Category::term_);
315}
316
317const Link* CommonMetadata::GetLinkByType(Link::LinkType type) const {
318  for (size_t i = 0; i < links_.size(); ++i) {
319    if (links_[i]->type() == type)
320      return links_[i];
321  }
322  return NULL;
323}
324
325////////////////////////////////////////////////////////////////////////////////
326// Content implementation
327
328Content::Content() {
329}
330
331// static
332void Content::RegisterJSONConverter(
333    base::JSONValueConverter<Content>* converter) {
334  converter->RegisterCustomField(kSrcField, &Content::url_, &GetGURLFromString);
335  converter->RegisterStringField(kTypeField, &Content::mime_type_);
336}
337
338////////////////////////////////////////////////////////////////////////////////
339// CommonMetadata implementation
340
341CommonMetadata::CommonMetadata() {
342}
343
344CommonMetadata::~CommonMetadata() {
345}
346
347// static
348template<typename CommonMetadataDescendant>
349void CommonMetadata::RegisterJSONConverter(
350    base::JSONValueConverter<CommonMetadataDescendant>* converter) {
351  converter->RegisterStringField(kETagField, &CommonMetadata::etag_);
352  converter->template RegisterRepeatedMessage<Author>(
353      kAuthorField, &CommonMetadata::authors_);
354  converter->template RegisterRepeatedMessage<Link>(
355      kLinkField, &CommonMetadata::links_);
356  converter->template RegisterRepeatedMessage<Category>(
357      kCategoryField, &CommonMetadata::categories_);
358  converter->template RegisterCustomField<base::Time>(
359      kUpdatedField, &CommonMetadata::updated_time_, &util::GetTimeFromString);
360}
361
362////////////////////////////////////////////////////////////////////////////////
363// ResourceEntry implementation
364
365ResourceEntry::ResourceEntry()
366    : kind_(ENTRY_KIND_UNKNOWN),
367      file_size_(0),
368      deleted_(false),
369      removed_(false),
370      changestamp_(0),
371      image_width_(-1),
372      image_height_(-1),
373      image_rotation_(-1) {
374}
375
376ResourceEntry::~ResourceEntry() {
377}
378
379bool ResourceEntry::HasFieldPresent(const base::Value* value,
380                                    bool* result) {
381  *result = (value != NULL);
382  return true;
383}
384
385bool ResourceEntry::ParseChangestamp(const base::Value* value,
386                                     int64* result) {
387  DCHECK(result);
388  if (!value) {
389    *result = 0;
390    return true;
391  }
392
393  std::string string_value;
394  if (value->GetAsString(&string_value) &&
395      base::StringToInt64(string_value, result))
396    return true;
397
398  return false;
399}
400
401// static
402void ResourceEntry::RegisterJSONConverter(
403    base::JSONValueConverter<ResourceEntry>* converter) {
404  // Inherit the parent registrations.
405  CommonMetadata::RegisterJSONConverter(converter);
406  converter->RegisterStringField(
407      kResourceIdField, &ResourceEntry::resource_id_);
408  converter->RegisterStringField(kIDField, &ResourceEntry::id_);
409  converter->RegisterStringField(kTitleTField, &ResourceEntry::title_);
410  converter->RegisterCustomField<base::Time>(
411      kPublishedField, &ResourceEntry::published_time_,
412      &util::GetTimeFromString);
413  converter->RegisterCustomField<base::Time>(
414      kLastViewedField, &ResourceEntry::last_viewed_time_,
415      &util::GetTimeFromString);
416  converter->RegisterRepeatedMessage(
417      kFeedLinkField, &ResourceEntry::resource_links_);
418  converter->RegisterNestedField(kContentField, &ResourceEntry::content_);
419
420  // File properties.  If the resource type is not a normal file, then
421  // that's no problem because those feed must not have these fields
422  // themselves, which does not report errors.
423  converter->RegisterStringField(kFileNameField, &ResourceEntry::filename_);
424  converter->RegisterStringField(kMD5Field, &ResourceEntry::file_md5_);
425  converter->RegisterCustomField<int64>(
426      kSizeField, &ResourceEntry::file_size_, &base::StringToInt64);
427  converter->RegisterStringField(
428      kSuggestedFileNameField, &ResourceEntry::suggested_filename_);
429  // Deleted are treated as 'trashed' items on web client side. Removed files
430  // are gone for good. We treat both cases as 'deleted' for this client.
431  converter->RegisterCustomValueField<bool>(
432      kDeletedField, &ResourceEntry::deleted_, &ResourceEntry::HasFieldPresent);
433  converter->RegisterCustomValueField<bool>(
434      kRemovedField, &ResourceEntry::removed_, &ResourceEntry::HasFieldPresent);
435  converter->RegisterCustomValueField<int64>(
436      kChangestampField, &ResourceEntry::changestamp_,
437      &ResourceEntry::ParseChangestamp);
438  // ImageMediaMetadata fields are not supported by WAPI.
439}
440
441// static
442std::string ResourceEntry::GetHostedDocumentExtension(DriveEntryKind kind) {
443  for (size_t i = 0; i < arraysize(kEntryKindMap); i++) {
444    if (kEntryKindMap[i].kind == kind) {
445      if (kEntryKindMap[i].extension)
446        return std::string(kEntryKindMap[i].extension);
447      else
448        return std::string();
449    }
450  }
451  return std::string();
452}
453
454// static
455DriveEntryKind ResourceEntry::GetEntryKindFromExtension(
456    const std::string& extension) {
457  for (size_t i = 0; i < arraysize(kEntryKindMap); ++i) {
458    const char* document_extension = kEntryKindMap[i].extension;
459    if (document_extension && extension == document_extension)
460      return kEntryKindMap[i].kind;
461  }
462  return ENTRY_KIND_UNKNOWN;
463}
464
465// static
466int ResourceEntry::ClassifyEntryKindByFileExtension(
467    const base::FilePath& file_path) {
468#if defined(OS_WIN)
469  std::string file_extension = base::WideToUTF8(file_path.Extension());
470#else
471  std::string file_extension = file_path.Extension();
472#endif
473  return ClassifyEntryKind(GetEntryKindFromExtension(file_extension));
474}
475
476// static
477DriveEntryKind ResourceEntry::GetEntryKindFromTerm(
478    const std::string& term) {
479  if (!StartsWithASCII(term, kTermPrefix, false)) {
480    DVLOG(1) << "Unexpected term prefix term " << term;
481    return ENTRY_KIND_UNKNOWN;
482  }
483
484  std::string type = term.substr(strlen(kTermPrefix));
485  for (size_t i = 0; i < arraysize(kEntryKindMap); i++) {
486    if (type == kEntryKindMap[i].entry)
487      return kEntryKindMap[i].kind;
488  }
489  DVLOG(1) << "Unknown entry type for term " << term << ", type " << type;
490  return ENTRY_KIND_UNKNOWN;
491}
492
493// static
494int ResourceEntry::ClassifyEntryKind(DriveEntryKind kind) {
495  int classes = 0;
496
497  // All DriveEntryKind members are listed here, so the compiler catches if a
498  // newly added member is missing here.
499  switch (kind) {
500    case ENTRY_KIND_UNKNOWN:
501    // Special entries.
502    case ENTRY_KIND_ITEM:
503    case ENTRY_KIND_SITE:
504      break;
505
506    // Hosted Google document.
507    case ENTRY_KIND_DOCUMENT:
508    case ENTRY_KIND_SPREADSHEET:
509    case ENTRY_KIND_PRESENTATION:
510    case ENTRY_KIND_DRAWING:
511    case ENTRY_KIND_TABLE:
512    case ENTRY_KIND_FORM:
513      classes = KIND_OF_GOOGLE_DOCUMENT | KIND_OF_HOSTED_DOCUMENT;
514      break;
515
516    // Hosted external application document.
517    case ENTRY_KIND_EXTERNAL_APP:
518      classes = KIND_OF_EXTERNAL_DOCUMENT | KIND_OF_HOSTED_DOCUMENT;
519      break;
520
521    // Folders, collections.
522    case ENTRY_KIND_FOLDER:
523      classes = KIND_OF_FOLDER;
524      break;
525
526    // Regular files.
527    case ENTRY_KIND_FILE:
528    case ENTRY_KIND_PDF:
529      classes = KIND_OF_FILE;
530      break;
531
532    case ENTRY_KIND_MAX_VALUE:
533      NOTREACHED();
534  }
535
536  return classes;
537}
538
539void ResourceEntry::FillRemainingFields() {
540  // Set |kind_| and |labels_| based on the |categories_| in the class.
541  // JSONValueConverter does not have the ability to catch an element in a list
542  // based on a predicate.  Thus we need to iterate over |categories_| and
543  // find the elements to set these fields as a post-process.
544  for (size_t i = 0; i < categories_.size(); ++i) {
545    const Category* category = categories_[i];
546    if (category->type() == Category::CATEGORY_KIND)
547      kind_ = GetEntryKindFromTerm(category->term());
548    else if (category->type() == Category::CATEGORY_LABEL)
549      labels_.push_back(category->label());
550  }
551}
552
553// static
554scoped_ptr<ResourceEntry> ResourceEntry::ExtractAndParse(
555    const base::Value& value) {
556  const base::DictionaryValue* as_dict = NULL;
557  const base::DictionaryValue* entry_dict = NULL;
558  if (value.GetAsDictionary(&as_dict) &&
559      as_dict->GetDictionary(kEntryField, &entry_dict)) {
560    return ResourceEntry::CreateFrom(*entry_dict);
561  }
562  return scoped_ptr<ResourceEntry>();
563}
564
565// static
566scoped_ptr<ResourceEntry> ResourceEntry::CreateFrom(const base::Value& value) {
567  base::JSONValueConverter<ResourceEntry> converter;
568  scoped_ptr<ResourceEntry> entry(new ResourceEntry());
569  if (!converter.Convert(value, entry.get())) {
570    DVLOG(1) << "Invalid resource entry!";
571    return scoped_ptr<ResourceEntry>();
572  }
573
574  entry->FillRemainingFields();
575  return entry.Pass();
576}
577
578// static
579std::string ResourceEntry::GetEntryNodeName() {
580  return kEntryNode;
581}
582
583////////////////////////////////////////////////////////////////////////////////
584// ResourceList implementation
585
586ResourceList::ResourceList()
587    : start_index_(0),
588      items_per_page_(0),
589      largest_changestamp_(0) {
590}
591
592ResourceList::~ResourceList() {
593}
594
595// static
596void ResourceList::RegisterJSONConverter(
597    base::JSONValueConverter<ResourceList>* converter) {
598  // inheritance
599  CommonMetadata::RegisterJSONConverter(converter);
600  // TODO(zelidrag): Once we figure out where these will be used, we should
601  // check for valid start_index_ and items_per_page_ values.
602  converter->RegisterCustomField<int>(
603      kStartIndexField, &ResourceList::start_index_, &base::StringToInt);
604  converter->RegisterCustomField<int>(
605      kItemsPerPageField, &ResourceList::items_per_page_, &base::StringToInt);
606  converter->RegisterStringField(kTitleTField, &ResourceList::title_);
607  converter->RegisterRepeatedMessage(kEntryField, &ResourceList::entries_);
608  converter->RegisterCustomField<int64>(
609     kLargestChangestampField, &ResourceList::largest_changestamp_,
610     &base::StringToInt64);
611}
612
613bool ResourceList::Parse(const base::Value& value) {
614  base::JSONValueConverter<ResourceList> converter;
615  if (!converter.Convert(value, this)) {
616    DVLOG(1) << "Invalid resource list!";
617    return false;
618  }
619
620  ScopedVector<ResourceEntry>::iterator iter = entries_.begin();
621  while (iter != entries_.end()) {
622    ResourceEntry* entry = (*iter);
623    entry->FillRemainingFields();
624    ++iter;
625  }
626  return true;
627}
628
629// static
630scoped_ptr<ResourceList> ResourceList::ExtractAndParse(
631    const base::Value& value) {
632  const base::DictionaryValue* as_dict = NULL;
633  const base::DictionaryValue* feed_dict = NULL;
634  if (value.GetAsDictionary(&as_dict) &&
635      as_dict->GetDictionary(kFeedField, &feed_dict)) {
636    return ResourceList::CreateFrom(*feed_dict);
637  }
638  return scoped_ptr<ResourceList>();
639}
640
641// static
642scoped_ptr<ResourceList> ResourceList::CreateFrom(const base::Value& value) {
643  scoped_ptr<ResourceList> feed(new ResourceList());
644  if (!feed->Parse(value)) {
645    DVLOG(1) << "Invalid resource list!";
646    return scoped_ptr<ResourceList>();
647  }
648
649  return feed.Pass();
650}
651
652bool ResourceList::GetNextFeedURL(GURL* url) const {
653  DCHECK(url);
654  for (size_t i = 0; i < links_.size(); ++i) {
655    if (links_[i]->type() == Link::LINK_NEXT) {
656      *url = links_[i]->href();
657      return true;
658    }
659  }
660  return false;
661}
662
663void ResourceList::ReleaseEntries(std::vector<ResourceEntry*>* entries) {
664  entries_.release(entries);
665}
666
667}  // namespace google_apis
668