Продолжаю копаться в Google Documents List API. И сегодня на повестке дня – работа с деревом каталогов и документов Google Docs. В силу того, что список документов Google представляет собой ни что иное как хранилище данных GData или, говоря проще, большую базу данных, этот список имеет ряд особенностей в плане расположения и структурирования данных нежели простой каталог с документами на жестком диске компьютера.
Введение
В чем принципиальное отличие? Дело в том, что как и в любой базе данных, любую запись в хранилище Google можно однозначно идентифицировать по ключу. В Google Docs ключом может выступать как Resource ID (идентификатор ресурса) так и уникальное значение – ETag. И это обстоятельство дает нам такую редко используемую на практике, но тем не менее возможность, как создание на одном уровне вложенности нескольких документов или папок (коллекций) с одним и тем же названием, что. как известно, невозможно сделать стандартными средствами той же ОС Windows. Например, можно создать вот такую структуру коллекций документов:
Так же само дело обстоит и с документами – можем создать два документа любого типа с одним и тем же названием в одной коллекции или в корне аккаунта. Собственно об этой особенности Google Docs я уже один раз упоминал в блоге. Возникает вполне резонный вопрос – как быть при построении такого дерева каталогов и документов?
Кажущийся на первый взгляд очевидным вариант – распознавать документ по его уникальному идентификатору на самом деле имеет несколько “подводных камней” с которыми так или иначе приходится сталкиваться и решать самыми различными способами, например, когда требуется полная синхронизация документов и каталогов между аккаунтом и локальной директорией. Однако эти проблемы никак не коснуться нас в случае, если нам необходимо решить только такую задачу как построение дерева каталогов и документов Google Docs – в этом случае Resource ID документа будет более, чем достаточно. Вот решением этой задачи мы и займемся сегодня.
1. Получаем исходные данные для построения дерева Google Docs.
Итак, что нам крайне необходимо для работы с деревом?
Во-первых, это значение Resource ID по которому мы будем распознавать элементы дерева.
Во-вторых, название элемента – его мы будем использовать для отображения элемента в дереве.
В-третьих, тип элемента. Этот параметр не столь значителен как первые два, но, тем не менее, в случае необходимости, его можно будет использовать, например, для того, чтобы присвоить узлу дерева определенное изображение из ImageList или правильно сформировать ссылку для скачивания документа и т.д.
В-четвертых, ID родительского элемента – по нему мы будем узнавать куда вставить тот или иной узел в дереве.
Этих значений нам будет достаточно. Добавить другие данные при необходимости не составит после сегодняшней работы никакого труда.
Естественно, для доступа к аккаунту нам понадобится компонент для ClientLogin, скачать который можно из репозитория GitHub.
Теперь приступим к реализации. В начале, как и положено, обратимся к официальной документации по API и посмотрим, что представляет из себя фид списка документов.
Все данные, которые нам будут необходимы, содержаться в элементах entry. Вот с ними мы и будем сейчас плотно работать. Про то как получить список документов Google Docs я также упоминал. Однако сегодня мы напишем класс без использования NativeXML и включим в него только те данные, которые нам необходимы для работы.
Класс назовем — TGoogleDoc:
uses ...xmlintf, xmldoc, TypInfo, StrUtils, SynaCode{Synapse}... type TDocType = (dtUnknown, dtDocument, dtPdf, dtFolder, dtPresentation, dtSpreadsheet, dtForm, dtFile, dtDrawing); TGoogleDoc = class private FTitle: string; FID: string; FParentID: string; FDocType: TDocType; public constructor Create(const AEntry: IXMLNode); destructor Destroy; override; procedure ParseXML(const AEntry: IXMLNode); property Title: string read FTitle write FTitle; property ID: string read FID; property ParentID: string read FParentID; property DocType: TDocType read FDocType; end;
При создании в конструктор класса передается узел AEntry:IXMLNode. Если предоставленный узел удовлетворяет требованиям то есть значение не равно nil , то запускается процедура ParseXML, которая пробует разобрать все дочерние узлы и заполнить поля класса необходимыми значениями. В противном случае — Вы всегда имеете возможность вызвать метод ParseXML из своей программы.
Процедура ParseXML выглядит следующим образом:
procedure TGoogleDoc.ParseXML(const AEntry: IXMLNode); function GetDocType(const ResourceID: string): TDocType; var ParamStr: string; ID: Integer; begin ParamStr := copy(ResourceID, 1, pos(':', ResourceID) - 1); ID := GetEnumValue(TypeInfo(TDocType), 'dt' + ParamStr); if ID > -1 then Result := TDocType(ID) else Result := dtUnknown; end; var Node: IXMLNode; begin if (AEntry = nil) or (AEntry.NodeName <> cEntryTag) then Exit; try Node := AEntry.ChildNodes.First; while Assigned(Node) do begin if Node.NodeName = cTitleTag then FTitle := Node.Text else if Node.NodeName = cResourceIDTag then begin FID := Node.Text; FDocType := GetDocType(FID); end else if Node.NodeName='link' then begin if Node.Attributes['rel']='http://schemas.google.com/docs/2007#parent' then begin FParentID := ReverseString(Node.Attributes['href']); FParentID := DecodeURL(ReverseString(copy(FParentID, 1, pos('/', FParentID) - 1))); end; end; Node := Node.NextSibling; end; except on E: EXMLDocError do raise E.CreateFmt(rsParseError, [E.Message]) else raise Exception.Create(rsUnknownError); end; end;
Здесь во вложенной функции GetDocType происходит преобразование строкового значения (Resource ID) к типу документа TDocType. В остальном процедура принимает в параметре XML-узел и, проходя по всех дочерним элементам узла, заполняет необходимые поля класса.
Что касается конструктора класса, то он достаточно прост:
constructor TGoogleDoc.Create(const AEntry: IXMLNode); begin inherited Create; if AEntry<>nil then ParseXML(AEntry); end;
Можно сказать, что заготовка для работы с Google Docs у нас готова. Теперь, если Вам потребуется, чтобы в классе TGoogleDoc хранились другие сведения по документу, например, ссылки на скачивание/редактирование или информация по автору документа, то создавайте необходимые поля и дополняйте метод ParseXML — эти действия никак не нарушат работу с нашим будущим деревом, а даже наоборот — расширят возможности.
Но хранить каждый элемент списка в отдельной переменной было бы расточительно и малоэффективно – требуется какая-нибудь структура наподобие списка или массива в которой будет хранится весь список Google Docs. Так как на дворе уже Delphi XE, а забивать статью лишним кодом мне бы не хотелось, то я решил воспользоваться в дальнейшей работе не списками типа TList, а таким относительно новым средством как дженерики. Может это и будет выглядеть как стрельбы из пушки по воробьям, но место и время на разработку сэкономит точно. Опять же, если кому-то потребуется, то дописать несколько методов к классу и сделать простой TList, думаю, не затруднит.
Итак, создадим класс списка документов, который будет заполняться на основании полученного из сети фида. Назовем класс TGoogleDocList:
TGoogleDocList = class(TList) public constructor Create; destructor Destroy; override; procedure FillFromXML(XMLStream: TStream); end;
В этом классе определен метод FillFromXML в параметре которого необходимо передать любой поток, содержащий XML-данные (фид списка документов) — на основании содержимого этого потока будет заполняться наш список:
procedure TGoogleDocList.FillFromXML(XMLStream: TStream); var xmldoc: IXMLDocument; Node: IXMLNode; i: Integer; begin if not Assigned(XMLStream) or (XMLStream.Size = 0) then Exit; xmldoc := TXMLDocument.Create(nil); try xmldoc.LoadFromStream(XMLStream, xetUTF_8); if xmldoc.IsEmptyDoc then Exit; for i := 0 to xmldoc.DocumentElement.ChildNodes.Count - 1 do begin Node := xmldoc.DocumentElement.ChildNodes.GET(i); if Node.NodeName = cEntryTag then self.Add(TGoogleDoc.Create(Node)); end; finally xmldoc := nil end; end; constructor TGoogleDocList.Create; begin inherited Create; end; destructor TGoogleDocList.Destroy; begin Clear; inherited Destroy; end;
Как видите, с дженериком код оказался довольно скромным. Кому интересно, можете почитать целую серию переводов про дженерики в блоге Алексея Тимохина — очень полезная информация.
Класс документа есть, список для хранения документов есть, переходим к следующему шагу – непосредственно получению списка от Google. В принципе, здесь Вы можете определиться сами какие библиотеки использовать, как организовывать работу с запросами и т.д. Для сохранения целостности статьи я не поленился и написал ещё один небольшой класс для отправки запросов, который, опять же в случае надобности, Вы сможете потом приспособить под отправку любых запросов к API. Класс использует в работе библиотеку Synapse и называется TGoogleRequest:
TGoogleRequest = class private FAuthKey: string; FHTTP: THTTPSend; procedure SetAuthKey(const Value: string); public constructor Create; destructor Destroy; override; procedure GET(const URL: string; var Stream: TStream); property AuthKey: string read FAuthKey write SetAuthKey; end;
В классе, кроме конструктора и деструктора определены всего одно свойство и два метода один из которых выполняет необходимый нам GET-запрос:
procedure TGoogleRequest.GET(const URL: string; var Stream: TStream); begin FHTTP.Clear; FHTTP.Headers.Add('GData-Version: ' + cGDataVersion); FHTTP.Headers.Add('Authorization: GoogleLogin auth=' + FAuthKey); if FHTTP.HTTPMethod('GET', URL) and (FHTTP.ResultCode = 200) then Stream.CopyFrom(FHTTP.Document, FHTTP.Document.Size) else raise Exception.CreateFmt(rsGETError, [IntToStr(FHTTP.ResultCode) + ' ' + FHTTP.ResultString]); end;
Вот теперь можно сказать, что все исходные данные для построения дерева документов Google Docs у нас есть. Подведем небольшой итог первой части работы (ниже будет рассмотрен пример). На данном этапе работы у нас есть:
- Компонент ClientLogin для получения ключа авторизации в сервисе Google Docs
- Класс TGoogleRequest, который, используя этот ключ выполняет GET-запросы для получения списка документов
- Класс для хранения данных по документу – TGoogleDoc.
- Класс для хранения списка документов – TGoogleDocList.
Теперь можно сохранить весь код в отдельный модуль с названием GoogleDocument и перейти к части построения дерева.
2. Пишем свой компонент для дерева документов Google Docs
Какими свойствами должно обладать наше дерево? Как минимум быть таким же пушистым и зеленым как на рисунке.
Если Вы обратите внимание на рисунок выше (с коллекциями) то увидите, что корневым элементом дерева всегда будет коллекция с названием “Мои коллекции”. Что ж и мы предусмотрим такую возможность – пусть в нашем дереве будет не только всегда фигурировать корневой элемент, но и название этого элемента мы будем задавать самостоятельно.
Также, в аккаунте Google Docs коллекции всегда отображаются в дереве, а файлы – списком. Наше дерево будет обладать свойствами и списка Google Docs и свойствами дерева коллекций, то есть предусмотрим три режима построения дерева:
- только коллекции
- только файлы
- всё содержимое аккаунта
И третьей особенностью пусть будет то, что наше дерево будет хранить в своих узлах не только название документа, но и все данные по документу, включая ID, тип документа и т.д. – всё, что вы захотите хранить в классе TGoogleDoc. Это позволит нам в любой момент получать необходимую информацию по элементу списка без лишний поисков в списке документов – буквально в один клик.
Итак, добавляем в наш проект новый модуль. Назовем его GoogleTree. В uses подключаем следующие модули: SysUtils, Windows, Controls, Classes, ComCtrls, GoogleDocument.
Создаем класс наследник от TCustomTreeView со следующими полями и свойствами:
type TGoogleVirtualTree = class(TCustomTreeView) private FRootName: string; FDocuments: TGoogleDocList; FAssigned: boolean; FMode: TTreeMode; procedure SetRootName(Value: string); procedure SetMode(Value: TTreeMode); procedure GenerateTree(Path: string); function FindFolder(const aParentID: string; var aDocument: TGoogleDoc; Offset: Integer = 0): Integer; procedure Setup; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure AssignDocuments(ADocuments: TGoogleDocList); property Items; published ... property Mode: TTreeMode read FMode write SetMode; property RootName: string read FRootName write SetRootName; end;
Разберемся, что и для чего здесь определено.
Свойство RootName: string — задает имя корневого элемента дерева. При присвоении какого-либо значения этому свойству корневой узел в дереве создается автоматически в методе SetRootName:
procedure TGoogleVirtualTree.SetRootName(Value: string); begin if Length(Trim(Value))=0 then FRootName:=rsRootFolder else FRootName := Value; if Assigned(TopItem) then TopItem.Text := FRootName else begin try Self.Items.Add(TTreeNode.Create(Self.Items), RootName); with Self.TopItem do begin Data := TGoogleDoc.Create(nil); TGoogleDoc(Data).Title := RootName; end; except end; end; end;
Если мы попробуем присвоить свойству пустую строку, то корневой элемент создастся со значением по умолчанию:
resourcestring rsRootFolder = 'Мои документы';
Свойство Mode: TTreeMode — задает режим построения дерева. При этом класс TTreeMode определен следующим образом (три режима, как и говорилось выше):
type TTreeMode = (mFiles, mFolders, mBoth);
Опять же, в случае, если меняется значение этого свойства, то дерево пробует перестроится под нужный режим автоматически в методе SetMode:
procedure TGoogleVirtualTree.SetMode(Value: TTreeMode); begin FMode := Value; Setup; end;
Здесь мы вначале вычищаем все, что было ранее в узлах дерева, затем пересоздаем корневой элемент и после этого переходим к построению обновленного дерева. При этом по флагу FAssigned определятся — стоит ли вообще, что-либо менять, есть ли в списке хотя бы один документ для показа его в дереве.
Теперь переходим к методам. Первый метод, который передает сведения о составе документов – это метод AssignDocuments. Так как в работе со списком мы использовали дженерик, то метод получился следующий:
pprocedure TGoogleVirtualTree.AssignDocuments(ADocuments: TGoogleDocList); begin FDocuments.Clear; if Assigned(ADocuments) then begin FDocuments.AddRange(ADocuments); FAssigned := FDocuments.Count > 0; Setup; end; end;
Здесь мы вначале очищаем предыдущий список документов, а затем копируем все, что передано в параметре одним, пока ещё новым для нас методом, AddRange, который является стандартным для дженерика. В методе Setup происходит проверка состояния списка и построение дерева:
procedure TGoogleVirtualTree.Setup; begin if FAssigned then begin Items.Clear; SetRootName(FRootName); GenerateTree(''); end; end;
То есть, если список документов содержит хотя бы один элемент, то дерево полностью очищается, устанавливается корневой элемент, а затем строится новое дерево. Для построения используется метод GenerateTree:
procedure TGoogleVirtualTree.GenerateTree(Path: string); procedure AddElement(Parent: TTreeNode; Document: TGoogleDoc); var Add: boolean; begin case Mode of mFolders: Add := (Document.DocType = dtFolder); mBoth: Add := true; end; if Add then Self.Items.AddChild(Parent, Document.Title).Data := pointer(Document) end; var found, i: Integer; Folder: TGoogleDoc; begin if (Mode = mFolders) or (Mode = mBoth) then begin found := FindFolder(Path, Folder); while (found > -1) do begin if (Folder.ParentID = '') then AddElement(TopItem, Folder) else for i := Items[Items.Count - 1].Parent.Index to Items.Count - 1 do if Items[i].Data <> nil then if TGoogleDoc(Items[i].Data).ID = Folder.ParentID then begin AddElement(Items[i], Folder); break; end; if (Folder.DocType = dtFolder) and (Mode <> mFiles) then GenerateTree(Folder.ID); found := FindFolder(Path, Folder, found + 1); end; end else begin for i := 0 to FDocuments.Count - 1 do if FDocuments[i].DocType <> dtFolder then Items.AddChild(Parent, FDocuments[i].Title).Data := pointer(FDocuments[i]) end; end;
В этом методе, в зависимости от заданного режима, выбирается одна из веток if..then..else и строится дерево. Самым простым с точки зрения построения, является режим mFiles — когда требуется просто отсеять элементы с типом dtFolder, а все остальные выставить дочерними к родительскому узлу — сформировать список документов. Эта операция происходит в else.
Более сложные манипуляции со списком происходят, когда выбран режим mFoders или mBoth — тогда мы выстраиваем дерево, используя рекурсию. При этом в для построения используются два дополнительных метода. Первый метод — FindFolder. Выполняет поиск элемента с заданным значением ParentId, то есть чью-то «дочку». При этом, для ускорения работы в метод можно передавать третьим параметром отступ от начала списка (offset: integer):
function TGoogleVirtualTree.FindFolder(const aParentID: string; var aDocument: TGoogleDoc; Offset: Integer): Integer; var i: Integer; begin for i := Offset to FDocuments.Count - 1 do if FDocuments[i].ParentID = aParentID then begin aDocument := FDocuments[i]; Result := i; Exit; end; Result := -1; end;
Второй метод — это вложенная процедура AddElement, которая проверяет соответствие элемента заданному режиму построения и в случае, если элемент подходит, то вставляет его в дерево. Код процедуры AddItem представлен в листинге выше.
Собственно на этом все – компонент готов и осталось его только зарегистрировать и проверить на работоспособность. Добавляем в модуль процедуру:
procedure Register; implementation procedure Register; begin RegisterComponents('BuBa Group', [TGoogleVirtualTree]); end;
Сохраняем модуль с названием GoogleTree.pas и переходим к третьему этап работы — регистрации компонента и его тестированию.
3. Тестируем работу дерева документов Google Docs
Создадим в Delphi новую группу проектов как показано на рисунке:
Пакет GoogleVirtualTree содержит два только что написанных модуля. С помощью него мы и зарегистрируем наше дерево в палитре компонентов. Жмем на пакете правой кнопкой мыши и выбираем в меню “Install”.
Теперь, как я и говорил в самом начале, для работы нам нужен компонент ClientLogin – качаем его их репозитория и также устанавливаем.
Теперь открываем главную форму приложения GTree.exe и укладываем на ней компоненты как показано на рисунке:
В uses подключаем модуль GoogleDocument (GoogleTree подключится после того как бросите на форму компонент дерева). В секции private класса формы задаем две переменные:
private FGoogleDocs: TGoogleDocList; FRequest: TGoogleRequest;
У компонента GoogleLogin1 свойство Service устанавливаем равным writley (сервис документов Google). Теперь переходим к обработчикам событий. На OnCreate формы пишем:
procedure TfMain.FormCreate(Sender: TObject); begin FGoogleDocs := TGoogleDocList.Create; FRequest := TGoogleRequest.Create; end;
На OnDestroy формы:
procedure TfMain.FormDestroy(Sender: TObject); begin FGoogleDocs.Destroy; FRequest.Destroy; end;
У компонента GoogleLogin1 создаем обработчик OnAutorization:
resourcestring rsOk = 'Авторизация прошла успешно'; rsFail = 'Авторизация не удалась. Проверьте логин и пароль'; procedure TfMain.GoogleLogin1Autorization(const LoginResult: TLoginResult; Result: TResultRec); begin if LoginResult = lrOk then begin FRequest.AuthKey := Result.Auth; ShowMessage(rsOk); end else raise Exception.Create(rsFail); end;
Обработчик кнопки Login напишем следующим образом:
procedure TfMain.btnLoginClick(Sender: TObject); begin GoogleLogin1.Email := edEmail.Text; GoogleLogin1.Password := edPass.Text; GoogleLogin1.Login(); end;
Теперь, в случае успешного логина, переменная FRequest получит необходимое значение ключа и можно будет получать список документов. Пишем обработчик кнопки «Получить список»:
procedure TfMain.btnGetListClick(Sender: TObject); var Stream: TStream; begin Stream := TMemoryStream.Create; try FRequest.GET ('https://docs.google.com/feeds/default/private/full?showfolders=true', Stream); FGoogleDocs.FillFromXML(Stream); lbDocCount.Caption := IntToStr(FGoogleDocs.Count); finally Stream.Free; end; end;
Здесь мы загружаем полный список документов, включая коллекции, передаем полученный поток в переменную списка FGoogleDocs и заполняем список значениями. Для построения дерева у нас есть отдельная кнопка «Построить дерево», обработчик которой выглядит так:
procedure TfMain.btnTreeClick(Sender: TObject); begin GoogleVirtualTree1.AssignDocuments(FGoogleDocs); end;
Этой строки достаточно чтобы построить дерево. Группа RadioButton’ов с помощью которых меняются режимы построения дерева имеет такой обработчик OnClick:
procedure TfMain.rgTreeModeClick(Sender: TObject); begin case rgTreeMode.ItemIndex of 0: GoogleVirtualTree1.Mode := mFiles; 1: GoogleVirtualTree1.Mode := mFolders; 2: GoogleVirtualTree1.Mode := mBoth; end; end;
И последний обработчик — это обработчик нашего дерева, с помощью которого мы будем выводить на панель информацию по выделенному элементу дерева:
procedure TfMain.GoogleVirtualTree1Click(Sender: TObject); begin if GoogleVirtualTree1.Selected <> nil then begin with TGoogleDoc(GoogleVirtualTree1.Selected.Data) do begin case DocType of dtUnknown: lbDocType.Caption := 'Неизвестен'; dtDocument: lbDocType.Caption := 'Документ'; dtPdf: lbDocType.Caption := 'PDF'; dtFolder: lbDocType.Caption := 'Коллекция'; dtPresentation: lbDocType.Caption := 'Презентация'; dtSpreadsheet: lbDocType.Caption := 'Таблица'; dtForm: lbDocType.Caption := 'Форма'; dtFile: lbDocType.Caption := 'Файл'; dtDrawing: lbDocType.Caption := 'Картинка'; end; lbTitle.Caption:=Title; lbID.Caption:=ID; lbIDParent.Caption:=ParentID; end; end; end;
Проверяем работу программы. Запускаем проект, вводим логин и пароль и последовательно жмем «Логин», «Получить список», «Построить дерево». Если введены правильные логин и пароль, то Вы получите примерно такую картинку:
Достаточно ткнуть на любой режим построения дерева и дерево будет перестраиваться. Вот, например, режим “Только папки”:
Все данные для заполнения сведений документа вытаскиваются из полей Data соответствующих узлов, что, как я и говорил вначале, избавляет нас от лишних операций поиска необходимого документа в списке.
Книжная полка
Описание: Рассмотрены практические вопросы по разработке клиент-серверных приложений в среде Delphi 7 и Delphi 2005 с использованием СУБД MS SQL Server 2000, InterBase и Firebird. Приведена информация о теории построения реляционных баз данных и языке SQL. Освещены вопросы эксплуатации и администрирования СУБД.
|
||
Название: О чем не пишут в книгах по Delphi
Описание: Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP и др.
|