SharePoint 開発の本質 (3) : ワークフロー フィーチャー

本内容は、SharePoint Server 2007 をベースに記載しています。(2008 年に記載したコラムです。コラム終了のため、こちらに移動しました . . .)
最新の SharePoint 2010 を使用した製品開発手法については、「SharePoint Server 開発 サンプル コード集 : 10 行コードで作る .NET アプリケーション」を参照してください。

前回まではユーザーインタフェース (UI) 構築を中心に話をしてきました。今回は、SharePoint 上でのプロセス実装としてワークフローを学び、この章では、その基本となる「考え方」を中心に説明をします。これまで述べてきた発想はここでも役に立ちます。つまり、ベースは「.NET」であり、そこに SharePoint 独自のフレームワークをかぶせてエンドユーザーに使いやすくしているという基本思想です。

SharePoint 開発の本質

 

WF の理解

.NET Framework では、バージョン 3.0 以降、Windows Workflow Foundation (以下 WF と記載します) という新しいフレームワークが追加されました。SharePoint の UI が ASP.NET のフレームワークをベースとして構築されていたように、SharePoint のワークフローも、この WF という .NET 上のフレームワークをベースに構築されています。一般のエンドユーザーはここに記載している WF を理解する必要はありませんが、製品レベル開発などをおこなうプロフェッショナル開発者がこの SharePoint ワークフローを快適に乗りこなしていくためには、WF への理解が必要不可欠となってきます。
WF は、例えて言うなら、注文組立型のパソコンのようなものです。こうしたパソコンでは、「プロセッサが必要で、メモリが必要で、、、」などその動きの大きなスペックは決まっていますが、そこにどのようなデバイスを入れるかは自由に組み合わせができます。WF では、自分だけの独自な (世界にただ 1 つだけの) デバイスを作って組み込むことさえもできます。ミッションクリティカルな業務で使うなら独自部品も組み合わせて思い切り豪華に、また作って遊ぶだけなら必要な最低限のものだけを使って組み立てるといった具合です。
では、基本的な動きを理解していただくため、WF の構造を SharePoint の実装と照らし合わせながら見ていきましょう。

補足 : .NET Framework のバージョン 3.0 が登場した当初の開発環境は、Visual Studio 2005 の拡張機能 (アドイン) として提供されていました。このバージョンをあらかじめ含んだ Visual Studio の製品リリースとしては、Visual Studio 2008 が該当します。(Visual Studio 2008には、さらに上位の .NET Framework 3.5 が含まれます)

補足 : なお、WF は、WF 4 (.NET Framework 4 の WF) で、大きくベースが変更されているので、ご注意ください。

まず、WF のワークフローで使用するオブジェクトは、すべて、「ワークフロー ランタイム」 (WorkflowRuntime) と呼ばれる .NET のオブジェクトの上で動作をさせます。そのため、必ずこのランタイムオブジェクトを作成して開始 (StartRuntime) させる必要があります。しかし、SharePoint では、このランタイムオブジェクトは SharePoint 内部で暗黙裡に実行されており、この内部のランタイムオブジェクトを使った勝手な操作は禁止されていますので注意してください。
WF 上のワークフロー全体の動きを制御するには、「ワークフロー ランタイム サービス」 (WorkflowRuntimeService) と呼ばれる .NET のオブジェクトを上述のワークフロー ランタイムにどんどん追加していきます。ワークフローが実行される場面を想像してみてください。通常は、いくつかのワークフローがバックエンドで動作し、必要な処理に到達したときに何らかのアクションを要求するといった動作が求められるでしょう。この一般的な動作を実現するためには、「スケジューラーサービス」 (WorkflowSchedulerService) と呼ばれる「ワークフローランタイムサービス」を追加します。デフォルトでは、1 つ 1 つのワークフローが並列に実行できるように、それぞれのワークフローが異なるスレッドで実行されるようなスケジューラーサービスが追加されています。 (無論、SharePoint でも、このスケジューラーサービスは挿入されています。) また承認ワークフローなどのケースを想像してみてください。日をまたいで実行されるこのようなワークフローでは、ワークフローがインメモリに長く常駐している状態は望まないことでしょう。こうした動きを制御するには、ワークフローランタイムに「永続化サービス」 (WorkflowPersistenceService) と呼ばれる同じく「ワークフローランタイムサービス」の一種を挿入することによって、例えばアイドル時に自動的にワークフローをデータベースなどへ永続化 (メモリから退避) し、実行時は自動的にこれらストアからメモリ上にロードさせることができます。そして SharePoint においても、SharePoint の動作仕様に適合した独自の永続化サービス (SPWinOePersistenceService) が挿入されています。さらに、基本的なデータのやりとりにおいても、この「ワークフローランタイムサービス」が必要です。先ほどスケジューラーサービスや永続化サービスなどを説明しましたが、ワークフローはマルチスレッドである場合や、メモリからアンロードされている場合などがありますので、普通の .NET のクラスのようにメソッドやプロパティに直接アクセスするのではなく、「データ交換サービス」 (ExternalDataExchangeService) と呼ばれる「ワークフロー ランタイム サービス」を使って、ワークフローと、ワークフローの外の世界とのデータ交換 (連携) をおこなう必要があるのです。
先ほど WF を「組立型のパソコン」に例えましたが、WF は、何も入れないと、「基本的な配線しか入っていないただの箱」でしかないという点がご理解いただけるでしょう。

ここで注意すべきポイントがあります。上述の通り、これら数々の「ワークフローランタイムサービス」は、ワークフローランタイムに挿入 (追加) され、ワークフローラインタイムを使った独自な処理は SharePoint では禁止されていました。つまり、SharePoint ワークフローの開発では、例えば、既存の永続化サービスを独自なサービスに変更したり、独自にトラッキング (追跡) 用のサービスを追加したり、といった処理はできません。

補足 : 厳密には、中身をよく理解している開発者はこうした実装が可能かもしれませんが、ルール上、こうした実装をしてはいけません。(例えば、ホット フィックス / サービス パックの適用の際などに、動作が保証されません。)

SharePoint ワークフローのカスタマイズでは、あらかじめ準備されたワークフローランタイムサービスしか使用できず、上述の「データ交換サービス」においても、「SharePoint 上のタスクとワークフローの連携」、「SharePoint 上のリストアイテムとワークフローの連携」などの限られた種類 (全部で 4 種類です) のデータ交換 (連携) しかおこなうことはできません。これはほとんどの場合には問題ありませんが、例えば、Web サービス/WCF サービスとワークフローを連携させながら動かしたい場合などにはしばしば問題になります。

補足 : こうしたケースでは、外部システム連携用の SharePoint タスクを作成して、このタスクをワークフローと連携させて、Web サービスを使って外部からこのタスクを変更する、などの実装が必要となります。また、このようにワークフローの中の個々のアクティビティと外部のシステムを連携させるのではなく、SharePoint のワークフローどうしを上位の別のプロセス (あるいはシステム) で統合連携させたい場合には、BizTalk Server が使用できます。
なお、SharePoint 2010 では、ローカル サービス を使って、こうした外部連携の機能を (開発者が) プラグインできるように拡張されています。

さて、ワークフローの流れ (フロー) の組み立てについてはまだ言及していませんでした。ワークフロー上で動作する個々の「振る舞い」は、すべて「アクティビティ」と呼ばれる .NET のオブジェクトが担当しています。このアクティビティには、その中にアクティビティを含める (ネストさせる) ことができる Composite アクティビティ (複合アクティビティ) というクラスもあり、実は、アクティビティの逐次実行や、While、If/Else などの制御、さらには並列実行なども、すべてこの Composite アクティビティ (厳密には、Composite アクティビティから継承されたクラス) を使って、これらのアクティビティの中にアクティビティをネストさせることでさまざまな制御を実装できるようになっています。つまり、ワークフローそのものも「アクティビティ」の 1 種であり、フローの組み立て自体もシンプルな概念の「組み合わせ」になっていることがおわかりいただけるでしょう。
WF には、開発者が必要とするアクティビティのいくつかが標準アクティビティとしてあらかじめ用意されています。例えば、ワークフローを停止するためのアクティビティや、前述のデータ交換サービスを使って処理をおこなうためのアクティビティなどさまざまです。SharePoint では、この WF が標準で提供している基本的なアクティビティ (Base Activity Library) に、SharePoint 独自の「タスクの作成」アクティビティ (CreateTask アクティビティ) 、「E メールの送信」アクティビティ (SendEMail アクティビティ) などのアクティビティが追加されているだけです。ワークフローの組み立ては、WF が提供している標準アクティビティとこれらの SharePoint 用のアクティビティを組み合わせて実装するという極めてシンプルなものです。

 

SharePoint 独自のフレームワーク

つぎに、SharePoint ワークフローだけがもつ独自のフレームワークについても説明をおこなっておきましょう。

実は SharePoint の世界では、同じ WF を使って構築されたワークフローであっても、その利用用途にあわせ 2 種類のワークフローが存在しています。 (これは、SharePoint 独自の概念です)

  • ワークフロー (Workflow)
    SharePoint のワークフローは、必ず、特定のリスト (またはコンテンツタイプ) に関連付いています。SharePoint Designer 2007 でワークフローを作成する場合は、ワークフローを作成すると同時にどのリストに関連付けるかを指定して構築をおこないます。このようにして作成したワークフローは、現場の業務管理者などが必要に応じ (リモートなどから) 作成と配置をおこなって迅速に利用することができますが、その一方で、他のリストへの設定や移動などはおこなえません。
  • ワークフロー テンプレート (Workflow Template)
    SharePoint では、「承認」ワークフローなどの頻繁に利用されるワークフローがデフォルトで入っていて、エンドユーザーは、好みのリスト (またはコンテンツタイプ) でこのインストールされているワークフローを自由に設定することができるようになっています。
    こうしたワークフローは、上記とは異なり、SharePoint に組み込まれたフィーチャー (機能) としてインストールされていて、開発者によるコーディングと、管理コマンドによるインストール作業が必要となりますが、SharePoint 上の自由なリスト (またはコンテンツタイプ) で設定 (再利用) することができるようになっています。

これを踏まえ、SharePoint を使用してワークフローを作成したい場合には、目的に応じて以下の 3 つの方法のいずれかを選択することができることをおぼえておいてください。

方法 ツール(その利用者) 内容
ワークフローの作成 SharePoint Designer
(現場業務の管理者)
特定のリストなどに関連付いたワークフローを SharePoint Designer を使って、ウィザード形式で、コードを記述することなく作成できます。
ワークフローテンプレートの作成 Visual Studio
(開発者)
Visual Studio を使用して、再利用可能なワークフローテンプレートを開発し、SharePoint 上に配置できます。
アクティビティの作成 Visual Studio
(開発者)
SharePoint Designer の利用者がワークフロー作成時に選択可能なカスタムのアクティビティを Visual Studio を使用して開発し、SharePoint 上に配置できます。

表のように、一般のエンドユーザー (現場業務の管理者) は 1 番目の方法だけをおぼえておけば問題ありません。そもそも彼らは、SharePoint に「ワークフロー」と「ワークフローテンプレート」が存在するという事実も知る必要はありません。一方でプロフェッショナル開発者は、WF の理解、「ワークフロー」と「ワークフローテンプレート」の理解など、仕組みの根本的な理解が必要とされます。

しかし、プロフェッショナル開発者は、エンドユーザーをより自由な世界へ導くことができる強力な武器を手にしています。例えば、SharePoint Designer 2007 によるワークフロー作成 (表の 1 番目の方法) では、事前に SharePoint Designer に登録されているアクティビティの中から必要なものを選択して組み合わせていくことしかできません (条件分岐は可能です)。よくあるシナリオとして、承認行為をおこなうために上長の取得をおこないたくなりますが、こうしたアクティビティも SharePoint Designer では標準で用意されていません。どの人事データベースを見れば良いのか、Active Directory を確認すれば良いのか、などがわからないためです。こうした点を回避する手段として、3 番目のカスタムのアクティビティ開発があります。Visual Studio で、ループの処理や上長の取得処理などを含むアクティビティを開発して SharePoint サーバーに配置をすれば、SharePoint Designer を使用する一般のエンドユーザーは、こうしたプロフェッショナル開発者が構築したアクティビティを使用して今まで不可能であったワークフローを構築することができます。また複雑なワークフローで、なおかつ頻繁に扱われるものは、あらかじめ 2 番目の方法で作成とインストールをしておき、エンドユーザーに、彼らが使いたい箇所で設定してもらうことが可能です。

 

SharePoint ワークフローの “開発”

前述の通り、SharePoint のワークフローは WF (Windows Workflow Foundation) をベースに構築されており、大袈裟に表現すれば、そこに SharePoint 用の仕組みがかぶさっているだけというシンプルな構造であることがおわかり頂けるでしょう。では、「ワークフローテンプレート」の構築を例に、そうした点を皆さんに実感して頂こうと思います。
なお、以降で述べるサンプルは、「製品レベル」の 高度な開発者がその中身 (Inside) を理解できるようにするため実施しているサンプルです。実際の開発では Visual Studio が標準で持っている SharePoint 用のテンプレートやアクティビティなどを使用して構築します ので注意してください。(現実の開発では、下記の真似はしないようにしてください。)

くどいようですが SharePoint のワークフローは、結局のところ WF (Windows Workflow Foundation) のワークフローにすぎません。今回は、あえてそのことを示すため Visual Studio 2008 の[シーケンシャルワークフローライブラリ]のプロジェクトテンプレートを選択してプロジェクトを新規作成します。そして、SharePoint が提供するクラスやインタフェースが使用できるように Microsoft.SharePoint.dll へ参照を追加しておきます。

つぎに、アクティビティを追加して処理を記述していきますが、その際に使用されるのが前回説明した「データ交換サービス」です。ワークフローの中から SharePoint に対してタスク作成をおこなったり、逆に SharePoint 側でタスクが更新されたらワークフローの処理を進めるなど、SharePoint サーバーと作成したワークフローの間で処理を連携させる必要がありますが、こうしたワークフローと外部の連携では「データ交換サービス」を使用する必要がありました。そして、このデータ交換サービスを使ってワークフロー内部から外のシステム (今回は SharePoint サーバー) に処理を要求する場合、WF では CallExternalMethod アクティビティを使用します。逆に、外のシステム (今回は SharePoint Server) からの要求をワークフロー内部で処理するには HandleExternalEvent アクティビティを使用します。

補足 : WF のデータ交換サービスの使い方 (CallExtenalMethod や HandleExternalEvent の使用方法) については、「MSDN : ソリューションサンプル WF の使用」でサンプルを例に理解することができます。

SharePoint では、まず、ワークフローの開始と共に「ワークフローが開始された!」ことを必ず通知してきますので (この通知の中に、「どのサイトで開始されたか」、「どのドキュメントアイテムのワークフローか」などの情報が入っています)、ワークフローの最初の処理として、この外のイベントを HandleExternalEvent アクティビティで処理することからはじまることになります。
そこで、まず、ツールボックスから HandleExternalEvent アクティビティをドラッグアンドドロップします (下図)。

つぎに、SharePoint サーバーから送られているメッセージがインタフェースとして定義されていますので、このインタフェースを HandleExternalEvent アクティビティで受け取れるようにします。上図で、右下のプロパティウィンドウを使用して、[InterfaceType]として Microsoft.SharePoint.dll の ISharePointService を選択し (下図)、[EventName]として OnWorkflowActiveted (このイベントは選択した ISharePointService の中で定義されています)を選択します。

補足 : この際、CorrelationToken というプロパティも表示されますが、ここには適当な文字列を設定しておいてください。例えば、タスクを複数作成して、それらのタスクの変更状況を HandleExternalEvent でリスンしたい場合などに、複数のタスクのうちのどのタスクをリスンするか指定する必要があるでしょう。CorrelationToken は、こうした場合に使用する識別子ですので、今回は適当な値を設定しておけば充分です。

設定されたプロパティは、以下の通りになります。

このアクティビティでは、受け取ったイベントの詳細の情報は、上図のイベントのパラメータ e の中に入っています。上図でパラメータ e の箇所を選択することで、このパラメータを受け取る変数を作成することができますので、今回は、「workflowEventArgs」という変数を作成して、この変数にこの値を格納しておきます。

また、この HandleExternalEvent アクティビティをダブルクリックすると、このアクティビティが実行される際の処理をプログラムコードで記述できます。今回は、以下のコードを記述して、ワークフローを開始したユーザー ID と、このワークフローが設定されているアイテムのタイトルを変数に格納しておきます。

private string itemTitle;
private string creatorUser;
private void handleExternalEventActivity1_Invoked(object sender, ExternalDataEventArgs e)
{
 itemTitle = workflowEventArgs.properties.Item.Title;
 creatorUser = workflowEventArgs.properties.Originator;
}

つぎに、SharePoint のタスクを作成してみましょう。今度は、ワークフローインスタンスの内部から外のシステム (SharePoint サーバー) に処理を要求するため、CallExternalMethod アクティビティの出番です。

上記同様に、CallExternalMethod アクティビティをツールボックスからドラッグアンドドロップし、今度は、InterfaceType として Microsoft.SharePoint.dll の ITaskService を選択し、MethodName として CreateTask (ITaskService の中のメソッドです) を選択します。(また先ほどと同様に CorrelationToken の値も適当な文字列を設定しておきます。)

上図のように、今度は、指定するプロパティとして taskId や properties といった値が指定可能になっています。これらは、ここで選択した CreateTask メソッドの引数として SharePoint 側に渡されます。taskId には作成するタスクの ID を GUID 型で指定し、properties には、タスクのその他の属性 (例:タスクのタイトルや、アサイン担当者、カスタムの属性、など) を SharePoint の SPWorkflowTaskProperties 型で指定します。そこで、先ほどの HandleExternalEvent アクティビティ同様に、これらのプロパティを設定する変数をそれぞれ taskId、taskProperties という名前で作成し、さらに、先ほど同様にこのアクティビティをダブルクリックして、アクティビティ実行時のコードとして以下の通り記述します。(CallExternalMethod のコードは、先ほどの HandleExternalEvent のコードと異なりメソッドを呼び出す前に実行されます。)

private void callExternalMethodActivity1_MethodInvoking(object sender, EventArgs e)
{
 taskId = Guid.NewGuid();
 taskProperties.Title = itemTitle + "のタスクが割り当てられました";
 taskProperties.AssignedTo = creatorUser;
}

最後に、このプロジェクトに署名を添付してリビルドをおこないます。すると、上記で構築したワークフローの処理がアセンブリ (.dll ファイル) として作成されます。

さて、このワークフローを SharePoint 上にどのように配置すれば良いのでしょうか?
前回の説明を思い出してください。SharePoint のワークフローには「ワークフロー」と「ワークフローテンプレート」という 2 種類の設定方法があり、後者は「フィーチャー」としてインストールする必要がありました。2 章で、.NET Framework のオブジェクトと SharePoint 独自の「フィーチャー」の違いについて説明しました。フィーチャーとして配置すると利用者にとっては使いやすいですが、開発者にとってはそのフィーチャーに関するさまざまな情報を XML 形式で定義しておく必要がありました。2 章では Visual Studio が自動でこの XML ファイルを作成してくれていましたが、今回はこの定義ファイルを自作してみましょう。

以下の 2 種類のファイルを作成します。

feature.xml

<?xml version="1.0" encoding="utf-8" ?>
<FeatureId="7075DF62-387B-4165-8B7C-D528578FCDD9"
  Title="デモ用のスクラッチワークフロー機能"
  Description="Tech Fielders のデモとして試しに作成しました"
  Version="1.0.0.0"
  Scope="Site"
  ReceiverAssembly="Microsoft.Office.Workflow.Feature, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
  ReceiverClass="Microsoft.Office.Workflow.Feature.WorkflowFeatureReceiver"
  xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests>
    <ElementManifest Location="MyWorkflow.xml" />
  </ElementManifests>
  <Properties>
    <Property Key="GloballyAvailable" Value="true" />
    <Property Key="RegisterForms" Value="*.xsn" />
  </Properties>
</Feature>

MyWorkflow.xml

<Elements
  Id="5319F617-FC0A-4d52-A61C-0FEADA08F36D"
  xmlns="http://schemas.microsoft.com/sharepoint/">
  <Workflow
    Name="デモ用のスクラッチワークフロー"
    Description="Tech Fielders のデモとして試しに作成しました"
    Id="1C2B8DA4-05D0-46e0-8C19-EFA53F7DF191"
    CodeBesideClass="MySPWorkflowDemo.Workflow1"
    CodeBesideAssembly="MySPWorkflowDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a5f5336fed11daa1">
    <Categories/>
    <MetaData>
      <StatusPageUrl>_layouts/WrkStat.aspx</StatusPageUrl>
    </MetaData>
  </Workflow>
</Elements>

補足 : 上記で、Feature 要素の Id 属性、Elements 要素の Id 属性、Workflow 要素の Id 属性のそれぞれには新しい GUID を作成して設定します。また、Workflow 要素の CodeBesideClass、CodeBesideAssembly には、上記で作成 (ビルド) したアセンブリにあわせて設定をおこないます。

これで準備は整いましたので、フィーチャーとして配置をしていきましょう。
作成されたアセンブリ (dll) を SharePoint から参照できるようにグローバルアセンブリキャッシュ (GAC) に登録し、SharePoint の管理コマンド (stsadm.exe) を使用して、以下の通りワークフローをフィーチャーとして SharePoint に組み込みます。

:: マニフェストのコピー
mkdir "%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATEFEATURESMySPWorkflowDemo"
copy feature.xml  "%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATEFEATURESMySPWorkflowDemo"
copy MyWorkflow.xml "%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATEFEATURESMySPWorkflowDemo"

:: フィーチャーのインストール
pushd %programfiles%common filesmicrosoft sharedweb server extensions12bin

stsadm -o installfeature -filename MySPWorkflowDemofeature.xml
stsadm -o activatefeature -filename MySPWorkflowDemofeature.xml -url http://localhost

popd

:: IIS の再起動
iisreset

これで配置は完了です。リストを作成し、登録された上記のワークフローを設定して、リストアイテムに対してワークフローを開始すると、実行したユーザーにタスクが新規作成されます。ワークフローは、アイテムの作成時などに自動的に開始させることも可能です。

ここでは、ワークフローの仕組みを理解していただくため敢えてそのベースとなっている WF (Windows Workflow Foundation) のみを使用して構築をおこないましたが、実際には、これらの設定を簡素化するために SharePoint 用の便利なテンプレートやアクティビティなどがいくつか用意されています。例えば、最初に設定した HandleExternalEvent アクティビティは、SharePoint の OnWorkflowActivated アクティビティを使用するともっと簡単に同じ処理を実現することができます (このアクティビティは、Microsoft.SharePoint.WorkflowActions.dll に含まれています)。
また、実際のワークフローの構築では、ここで述べた簡単なワークフローとは異なり While や IfElse などと組み合わせた高度なワークフローを構築する必要があるでしょう。ワークフロー開始時やタスクの実行時に独自なユーザーインタフェースを使用することも必要となるでしょう。あとはアクティビティや周辺コンポーネントを組み合わせて構成していくのみです。

また、今回は「ワークフローテンプレート」を構築しましたが、SharePoint Designer が作成する「ワークフロー」では、WF のアクティビティを使用してワークフローのコードを構築し、このコードを SharePoint サーバーに送信して、Web サービスを介して SharePoint サーバー側でコンパイルされています。(つまり、ここでも WF を使用してワークフローが構築されています)。より進んだ開発者は、こうした内部の動きを理解しておけば、自作のワークフローエディターまでも構築することが可能でしょう。

補足 : なお、SharePoint Designer が作成する WF のワークフローは、XML 形式のマークアップコードとして作成されます。

ここで見てきたように、WF を使用して構築したワークフローに SharePoint 用のフィーチャー構成をおこなうだけで SharePoint のワークフローテンプレートは完成します。WF を含む .NET Framework の知識があれば、あとはこのフレームワークをベースに SharePoint の追加の機能が実装されているだけであるという基本的な構造がおわかり頂けるでしょう。

補足 : SharePoint Workflow におけるより詳細のメカニズムと開発技術については、書籍「VSTOとSharePoint Server 2007 による開発技術」(翔泳社) に記載しました。

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s