Presentation is loading. Please wait.

Presentation is loading. Please wait.

Google App Engine API.

Similar presentations


Presentation on theme: "Google App Engine API."— Presentation transcript:

1 Google App Engine API

2 Outline 資料庫 網路溝通 圖形處理 工作排程 其他服務 Google App Engine API 專題

3 資料庫 網路溝通 圖形處理 工作排程 其他服務 Google App Engine API 專題

4 資料庫 資料庫的操作分成兩部分:Low-Level API 及 JDO。 Low-Level API: JDO:
Google App Engine 原始提供的資料庫操作方式。 JDO: 全名為 Java Data Object。 是一種標準介面,能夠將 Java 物件儲存至資料庫中。

5 Low-Level API 每一筆在資料庫的資料稱為 Entity。
圖中可以看到資料 R1 僅使用欄位 C1,R2 使用 C1 至 C3,R3 則是使用 C4 和 C5。

6 Low-Level API 範例一 在資料庫中新增三筆學生資料。
DatastoreService service = DatastoreServiceFactory.getDatastoreService(); Entity student1 = new Entity(“Student”); student1.setProperty("name", "michael"); // 在學生 (Student) 的表格中宣告一筆學生 student1,並給予他的名字為 michael。 Entity student2 = new Entity(“Student”); student2.setProperty(“name”, “alex”); student2.setProperty(“age”, 20); Calendar calendar = new GregorianCalendar(1991, 2, 23); Date date = calendar.getTime(); student2.setProperty("birthday", date); // 第二個學生 student2,給予姓名、年紀和生日分別為 alex、20 和 1991 年 2 月 23 日。 Entity student3 = new Entity(“Student”); student3.setProperty(“studentID”, “M09802XXX”); student3.setProperty("phoneNumber", "0900XXXXXX"); // 第三個學生 student3 則儲存他的學號和電話。

7 Low-Level API 範例二 Entity 建構子參數中可以指定資料的唯一鍵值。
Entity student1 = new Entity(“Student”); student1.setProperty(“name”, “michael”); Entity student2 = new Entity(“Student”, “Alex”); student2.setProperty(“name”, “alex”); student2.setProperty("age", 20); // student1 沒有指定,預設為流水號 // student2 的唯一鍵值為 ”Alex”。

8 Low-Level API 範例三 對資料庫進行查詢。 Query query = new Query("Student");
PreparedQuery pq = service.prepare(query); for (Entity each : pq.asIterable()) { String studentName = (String) each.getProperty(“name”); } // 在 for 迴圈中利用字串儲存表格中每一筆資料中 “name” 欄位的值。

9 Low-Level API 範例四 對資料庫進行有條件的查詢,並且排序資料。
如同使用 SQL 利用 where 和 order by 對資料庫做查詢一樣。 Query query = new Query(“Student”); query.addFilter("age", FilterOperator.LESS_THAN, 20); // 對此一 Query 加入查詢條件(年齡小於20)。 query.addSort("name", SortDirection.ASCENDING); // 以名字做為排序依據。 PreparedQuery pq = service.prepare(query); for (Entity each : pq.asIterable()) { String name = (String) each.getProperty(“name”); }

10 Low-Level API 範例五 對查詢進行筆數限制。 Query query = new Query(“Student”);
PreparedQuery pq = service.prepare(query); FetchOptions options = FetchOptions.Builder.withLimit(5); // 限制回傳的資料筆數為五筆。 for (Entity each : pq.asIterable(options)) { String name = (String) each.getProperty(“name”); }

11 Low-Level API 範例六 僅取得查詢結果的第一筆。
DatastoreService service = DatastoreServiceFactory.getDatastoreService(); Query query = new Query(“Student”); PreparedQuery pq = service.prepare(query); Entity first = pq.asSingleEntity(); // 使用asSingleEntity()取得第一筆資料。

12 Low-Level API 範例七 使用交易 (Transaction)。 交易中的任何一個地方出錯,則會復原所有在交易中所做的變更。
當 commit() 被執行後才算是完成整筆交易。 Transaction tx = service.beginTransaction(); try { // 交易過程 tx.commit(); } finally { if (tx.isActive()) tx.rollback(); }

13 Low-Level API 範例七 使用 Transaction 確保學生資料與電話資料能夠同時寫入資料庫中。
Transaction tx = service.beginTransaction(); try { Entity student = new Entity(“Student”); student.setProperty(“name”, “David”); student.setProperty(“age”, 18); service.put(student); Entity phoneNumber = new Entity(“PhoneNumber”, student.getKey()); phoneNumber.setProperty(“home”, “03-537XXXX”); phoneNumber.setProperty(“mobile”, “ XX”); service.put(phoneNumber); tx.commit(); } finally { if (tx.isActive()) tx.rollback(); }

14 JDO (Java Data Object) 使用 JDO 操作資料庫時,表格中的欄位都會填入特定的資料,或者填上 Null 表示沒有資料但仍有使用該欄位,這點有別於 Low-Level API。 R1 只有填入 C1 欄位的資料,但依然擁有所有欄位,因此其它欄位的資料為 Null。

15 JDO 定義資料類別 定義JDO的資料類別。 @PersistenceCapable public class Student {
// 類別名稱就如同是資料庫表格名稱。 @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; // 同樣有唯一鍵值。 @Persistent private String name; // 類別的資料成員如同是資料表的欄位,有欄位資料型態與名稱。 private String studentNo; private Date birthday;

16 JDO 與 PersistenceManager
基於方便性與速度考量,將其取得過程封裝成 PMF 類別。 public final class PMF { private static final PersistenceManagerFactory pmfInstance = JDOHelper.getPersistenceManagerFactory("transactions-optional"); private PMF() {} public static PersistenceManagerFactory get() { return pmfInstance; }

17 JDO 範例一 使用JDO新增一筆學生的資料。
PersistenceManager pm = PMF.get().getPersistenceManager(); Student student = new Student("Kevin", "B ", new GregorianCalendar(1990, 5, 4).getTime(), "ME"); // 建立一筆學生資料。 pm.makePersistent(student); // 使用PersistenceManager將學生資料寫入資料庫。

18 JDO 範例二 使用JDO新增一筆學生的資料並指定唯一鍵值。
Student student = new Student(“Jeff”, “B ”, new GregorianCalendar(1990, 3, 18).getTime(), “EE”); String studentNo = student.getStudentNo(); Key key = KeyFactory.createKey(Student.class.getSimpleName(), studentNo); // 將學號當做參數傳入並產生鍵值。 student.setKey(key); // 設定成此筆學生資料的唯一鍵值。 pm.makePersistent(student); Student student2 = pm.getObjectById(Student.class, studentNo); // 使用唯一鍵值取得剛剛新增的學生資料。

19 JDO 範例三 更新資料庫表格中一筆資料的欄位。
Student student = pm.getObjectById(Student.class, “B ”); student.setDepartment("CSIE"); // 取得學生資料後,修改學生的系別。 pm.makePersistent(student); // 將修改後的結果寫入資料庫。

20 JDO 範例四 刪除一筆資料庫中的資料。 Student student = pm.getObjectById(Student.class, “B ”); pm.deletePersistent(student); // 將學生做為 deletePersistent() 的參數。

21 JDO 範例五 對資料表格進行查詢。 Query query = pm.newQuery(Student.class);
List<Student> students = (List<Student>) query.execute(); // 宣告一個 Query,對學生資料表進行查詢。 // 回傳的結果需要自行轉型為 List。 for(Student each : students){ String name = each.getName(); }

22 JDO 範例六 設定搜尋條件。 Query query = pm.newQuery(Student.class);
query.setFilter("department == 'ME'"); // 只查詢系別為 ”ME” 的學生資料。 // setFilter() 如同 SQL where 條件。 List<Student> students = (List<Student>) query.execute(); for(Student each : students){ resp.getWriter().println(each.getName()); }

23 JDO 範例六 設定搜尋條件另一種做法。 以變數取代欲查詢之關鍵字。
query2.setFilter(“department == departmentParam”); query2.declareParameters(“String departmentParam”); List<Student> students2 = (List<Student>) query2.execute("ME");

24 JDO 範例七 限制多個條件以及回傳的筆數。
query.setFilter("department == departmentParam && name == nameParam"); // 同時設定兩個查詢條件。 query.setOrdering(“studentNo desc”); query.declareParameters(“String departmentParam, String nameParam”); query.setRange(0, 3); // 僅查詢第 0, 1, 2 筆的資料。 List<Student> students = (List<Student>) query.execute(“ME”, “Kevin”); for(Student each : students){ resp.getWriter().println(each.getName()+“ ”+each.getStudentNo()); }

25 JDO 資料關聯性 JDO能夠建立資料的關聯性。 “一對一”例子: “一對多”例子: “一對一”、”一對多”。
一個學生對應一個地址資訊(AddressInfo) “一對多”例子: 一個學生對應多個電話號碼資訊(PhoneNumber)

26 JDO 一對一(1/2) 定義地址資訊的資料類別。 @PersistenceCapable
public class AddressInfo { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private Student student; // 因為要夠能與學生(Student)產生關聯。地址資訊類別中需要加入一個 Student 的資料成員。 private String zipCode; // 郵遞區號 private String address; // 詳細地址

27 JDO 一對一(2/2) 學生資料類別也需要進行編輯。 @PersistenceCapable public class Student {
... @Persistent(dependent = "true") private AddressInfo addressInfo; // 加入上面兩行,JDO 便能夠自動將學生與地址建立一對一的關聯。

28 JDO 範例八 使用一對一的關聯建立學生與地址資料。
Student student = new Student(“Harry”, “B ”, new GregorianCalendar(1990, 6, 7).getTime(), “CSIE”); AddressInfo addressInfo = new AddressInfo("30012", "新竹市香山區五福路二段707號"); // 新增一筆地址資訊。 student.setAddressInfo(addressInfo); // 將地址資訊指定給該學生。 pm.makePersistent(student); // 寫入時只需要傳入學生,地址資訊會被自動的寫入。

29 JDO 一對多 定義電話號碼的資料類別。 @PersistenceCapable public class PhoneNumber {
@PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private Student student; // 與一對一相似,需要加入 Student 的資料成員以建立關聯。 private String type; // 號碼類型 private String number; // 電話號碼

30 JDO 範例九 使用一對多的關聯建立學生與電話號碼資料。
Student student = new Student(“Tom”, “B ”, new GregorianCalendar(1991, 9, 15).getTime(), “CSIE”); List<PhoneNumber> phoneNumbers = new ArrayList<PhoneNumber>(); // 新增一個 List 容器來存放電話號碼資訊。 PhoneNumber home = new PhoneNumber(“Home”, “03-518XXX”); PhoneNumber mobile = new PhoneNumber(“Mobile”, “0911XXXXXX”); phoneNumbers.add(home); phoneNumbers.add(mobile); // 新增兩筆不同類型的電話號碼,並加到 List 容器中。 student.setPhoneNumbers(phoneNumbers); // 將存放兩筆電話號碼的 List 指定給學生。 pm.makePersistent(student); // 寫入時只需要傳入學生。

31 資料庫 網路溝通 圖形處理 工作排程 其他服務 Google App Engine API 專題

32 網路溝通 這個部分將介紹以下三種Google App Engine的服務。 URLFetch Service Mail Service
寄送電子郵件 Channel Service 與使用者端建立通道連線,能即時傳遞雙向訊息。

33 URLFetch URLFetch服務能夠擷取存在於網路上的各種資訊。
Google App Engine提供了URLFetch的API給開發人員使用。 也可以使用Java函式庫中的java.net來進行。

34 URLFetch 範例一 使用java.net,透過URL取得台北天氣資訊。 將所取得的內容一行一行顯示在網頁上。
URL url = new URL(“ BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream())); String line; while ((line = reader.readLine()) != null) { resp.getWriter().println(line); }

35 URLFetch 範例二 使用URLFetch API取得圖片。
URLFetchService service = URLFetchServiceFactory.getURLFetchService(); HTTPResponse httpresp = service.fetch(new URL(“ resp.setContentType(“image/png”); resp.getOutputStream().write(httpresp.getContent());

36 URLFetch 範例三 使用URLFetch API取得文字類型的資料。
URLFetchService service = URLFetchServiceFactory.getURLFetchService(); HTTPResponse httpresp = service.fetch(new URL(“ resp.setContentType(“text/xml”); resp.getWriter().println(new String(httpresp.getContent()));

37 Mail Mail服務能夠讓開發者透過Google App Engine來寄送電子郵件。
可以使用Google App Engine的Mail API或者Java函式庫中的javax.mail。

38 Mail 範例一 使用javax.mail寄送電子郵件。 此範例中,寄件人為xxx@gmail.com,收件人為yyy@gmail.com。
Properties props = new Properties(); Session session = Session.getDefaultInstance(props, null); Message msg = new MimeMessage(session); msg.setFrom(new msg.addRecipient(Message.RecipientType.TO, new msg.setSubject(“GAE javax.mail test”); msg.setText(“中文測試”); Transport.send(msg);

39 Mail 範例二 使用Mail API寄送電子郵件。
MailService service = MailServiceFactory.getMailService(); MailService.Message msg = new MailService.Message( "GAE mail test", "google app engine mail service test"); // MailService.Message() 的參數分別為寄件人、收件人、主旨以及郵件內容。 service.send(msg); // 發送郵件。

40 Channel Channel服務能夠在使用者端與伺服端之間建立。
圖中,使用者端對伺服端要求一份含有JavaScript及特定token的HTML文件。文件中的JavaScript會在使用者端被執行,進而依token建立一條專屬的通道。 建立之後,使用者端與伺服端可以進行雙向溝通。

41 Channel 範例(1/3) 使用者端發出要求時,伺服端必須建立通道並回傳一份HTML給使用者端。
ChannelService channelService = ChannelServiceFactory.getChannelService(); String token = channelService.createChannel("xxx"); // 此範例以字串 ”xxx” 做為產生 token 的依據。 FileReader reader = new FileReader("channel-template"); // 將包含 JavaScript 的 HTML 範本 (channel-template) 讀取進來。 CharBuffer buffer = CharBuffer.allocate(1024); reader.read(buffer); String template = new String(buffer.array()); template = template.replaceAll("\\{\\{ token \\}\\}", token); // 將範本內容的 ”{{ token }}” 替換成前面產生的 token。 resp.setContentType(“text/html”); resp.setCharacterEncoding(“utf-8”); resp.getWriter().write(template); // 以 HTML 的方式呈現在使用者端。

42 Channel 範例(2/3) 使用者端從伺服端接收的HTML片段如下:
<script type="text/javascript" src="/_ah/channel/jsapi"></script> // 此 HTML 碼引入了一個由 Google App Engine 提供的 JavaScript 函式庫。 <script> onMessage = function(msg){ var div = document.getElementById(‘show’); div.innerHTML = msg.data; } channel = new goog.appengine.Channel(‘{{ token }}’); socket = channel.open(); socket.onmessage = onMessage; </script> // 這段JavaScript將會被執行,建立通道,並且將 onmessage 的事件處理者指定為 onMessage() 函式 。 // 當有訊息從伺服端發送過來,onMessage() 會自動被呼叫。 // 此範例中,onMessage() 會把伺服端傳來的訊息寫入 id 為 ”show” 的 div 標籤中。

43 Channel 範例(3/3) 使用者端可以利用一般的HTTP請求或Ajax等非同步的方式傳送訊息給伺服端。
伺服端則必須使用Channel API與使用者端通訊,方式如下: ChannelService channelService = ChannelServiceFactory.getChannelService(); channelService.sendMessage(new ChannelMessage("xxx", "message contents…")); // 在 ChannelMessage 中指定要發送的對象所屬的 token 以及要傳送的訊息內容。 // 再經由 ChannelService 的 sendMessage() 函式發送。

44 資料庫 網路溝通 圖形處理 工作排程 其他服務 Google App Engine API 專題

45 圖形處理 Google App Engine提供一組圖形操作的Image API,具有下列六項功能: 能夠處理的圖片格式: 改變圖片大小
自動調整顏色和亮度 圖片裁切 圖片旋轉 水平翻轉 垂直翻轉 能夠處理的圖片格式: JPEG, PNG, WEBP, GIF, BMP, TIFF 及 ICO

46 Image 範例一(1/2) 首先是改變圖片大小。 byte[] origImageData;
ImagesService service = ImagesServiceFactory.getImagesService(); Image origImage = ImagesServiceFactory.makeImage(origImageData); // 將圖片由 byte[] 轉換成 Image。 Transform resize = ImagesServiceFactory.makeResize(150, 150); // 將圖片大小設定 150*150。 Image newImage = service.applyTransform(resize, origImage); // 套用此變更。 byte[] newImageData = newImage.getImageData();

47 Image 範例一(2/2) 除了改變圖片大小外,還有下列五項功能。
Transform imFeelingLucky = ImagesServiceFactory.makeImFeelingLucky(); // 自動調整顏色和亮度。 Transform crop = ImagesServiceFactory.makeCrop(0.1, 0.0, 0.5, 0.5); // 圖片裁切。參數分別是圖片水平寬度的左邊界、垂直高度的上邊界、水平寬度的右邊界和垂直高度的下邊界。這些數值為圖片原始寬度和原始高度的比例。 Transform rotate = ImagesServiceFactory.makeRotate(90); // 圖片旋轉。只能填 90 度的倍數。 Transform horizontalFlip = ImagesServiceFactory.makeHorizontalFlip(); // 水平翻轉。 Transform verticalFlip = ImagesServiceFactory.makeVerticalFlip(); // 垂直翻轉。

48 Image 範例二 如果想一次套用多種功能,而不需要執行好幾次applyTransform(),可以使用CompostieTransform。 Transform resize = ImagesServiceFactory.makeResize(150, 150); Transform imFeelingLucky = ImagesServiceFactory.makeImFeelingLucky(); Transform crop = ImagesServiceFactory.makeCrop(0.0, 0.0, 0.5, 0.5); Transform rotate = ImagesServiceFactory.makeRotate(90); Transform horizontalFlip = ImagesServiceFactory.makeHorizontalFlip(); Transform verticalFlip = ImagesServiceFactory.makeVerticalFlip(); CompositeTransform ct = ImagesServiceFactory.makeCompositeTransform(); ct = ct.concatenate(resize).concatenate(imFeelingLucky) .concatenate(crop).concatenate(rotate) .concatenate(horizontalFlip).concatenate(verticalFlip); // 將所有功能合併至單一 Transform。可以自行改變套用的先後順序。 Image newImage = service.applyTransform(ct, origImage);

49 資料庫 網路溝通 圖形處理 工作排程 其他服務 Google App Engine API 專題

50 工作排程 工作排程在Google App Engine平台上有兩種意義: 接下來會依序介紹這兩種工作排程的機制:
一種是在佇列中安排逐項工作,當某一項工作執行完畢後,就會從佇列中被移除。 一種是固定時間後取得網路資訊的重複工作,這種工作只要輸入一次,之後便會按照固定的時間間隔重複執行。 接下來會依序介紹這兩種工作排程的機制: Queue Cron

51 Queue 範例 將欲執行的工作放入佇列(Queue)。
Queue queue = QueueFactory.getDefaultQueue(); // 取得預設的佇列。 queue.add(TaskOptions.Builder.withUrl(“/taskqueue_dowork?name=andy”).method(Method.GET)); queue.add(TaskOptions.Builder.withUrl(“/taskqueue_dowork”).method(Method.POST).param("name", "frank")); // 上面兩行是將工作加到佇列中,分別使用 GET 及 POST。 // 如果佇列裡沒有排定任何工作的話,被加入的工作會立即執行。

52 Cron 範例 使用Cron來執行例行性的工作。 要給Cron執行的工作只需要寫在設定檔(cron.xml)裡即可。
<url>/dailyannouncement</url> <description>Daily announcement service</description> <schedule>every day 16:00</schedule> <timezone>Asia/Taipei</timezone> </cron> 依據此設定檔,Cron每天下午四點會執行 /dailyannouncement。

53 資料庫 網路溝通 圖形處理 工作排程 其他服務 Google App Engine API 專題

54 其他服務 本節將介紹 Google App Engine 上的快取服務、使用者認證方法,以及適合存放大型檔案的 Blobstore。
快取服務(Memcache) 利用快取可以提升程式效率,另一方面也能夠節省 Google App Engine 上的存取額度。 使用者認證(Users) 透過 Google 帳號來進行使用者認證。 Blobstore 提供適合存放大型檔案的額外空間 (1GB)。 此功能需要付費使用。

55 Memcache 範例 利用快取服務將一些微量但卻經常讀取的資料快取在伺服器的記憶體當中。 Cache cache = null;
CacheFactory cacheFactory = CacheManager.getInstance().getCacheFactory(); cache = cacheFactory.createCache(Collections.emptyMap()); // 建立快取空間。 String sth = null; // 假設 sth 會被頻繁讀取。 if (cache.containsKey(“sth-key”)) { // 檢查快取資料中是否有鍵值 “sth-key” sth = (String) cache.get(“sth-key”); // 從快取中取得 sth } else { sth = GetDataFromDatastore(); // 從資料庫中取得 sth cache.put(“sth-key”, sth); // 將 sth 寫入快取,以便於下次使用。 }

56 Users 範例 將使用者認證導向 Google,由 Google 帳號來進行認證。
String thisURL = req.getRequestURI(); UserService userService = UserServiceFactory.getUserService(); if (userService.getCurrentUser() != null) { // 判斷目前的使用者是否有登入。 // 如果沒有登入則產生導向 Google 帳號認證頁面的連結,反之則產生登出連結。 resp.getWriter().println("<div>Hello, " + userService.getCurrentUser().get () + "!! <a href=\"" + userService.createLogoutURL(thisURL) // 產生登出連結。 + “\”>sign out</a></div>“); } else { resp.getWriter().println(“<div><a href=\”“ + userService.createLoginURL(thisURL) // 產生登入連結。 + "\">sign in</a></div>"); }

57 Blobstore 範例(1/4) 當開發者需要在 Google App Engine 上存放較大的檔案時,可以使用 Blobstore。
此範例分成三個部分: 上傳介面 (blobstore.jsp) 上傳後儲存 BlobKey 及檔案 (UploadBlob.java) 使用檔案 (ServeBlob.java)

58 Blobstore 範例(2/4) 以下是 blobstore.jsp 的片段程式碼: <body> <%
BlobstoreService service = BlobstoreServiceFactory.getBlobstoreService(); // 取得 Blobstore 服務。 %> <form action=“<%=service.createUploadUrl(”/upload“)%>” method=“post” enctype=“multipart/form-data”> // 表單的 action 由 Blobstore 服務產生。 // 上傳後會交由 ”/upload” 所對應到的 Servlet 處理 (UploadBlob.java)。 <input type=“file” name=“uploadFile” /> // 上傳檔案的識別名稱為 “uploadFile”。 <input type=“submit” value=“Submit” /> </form> </body>

59 Blobstore 範例(3/4) 以下是 UploadBlob.java 的片段程式碼:
Map<String, BlobKey> blobs = blobstoreService.getUploadedBlobs(req); // 從 Blobstore 服務中取得使用者上傳的表單欄位名稱,以及在 Blobstore 中對應的 BlobKey。 BlobKey blobKey = blobs.get(“uploadFile”); // 依上傳檔案的識別名稱取得對應的 BlobKey。 if (blobKey != null) { resp.sendRedirect(“/serve?blob-key=” + blobKey.getKeyString()); // 如果 BlobKey 存在,將 BlobKey 傳給另一個 Servlet 處理 (ServeBlob.java) }

60 Blobstore 範例(4/4) 以下是ServeBlob.java的片段程式碼:
BlobKey blobKey = new BlobKey(req.getParameter(“blob-key”)); // 取得由外面傳入的字串參數 ”blob-key”,轉換成 BlobKey 型別。 blobstoreService.serve(blobKey, resp); // 利用 serve() 將 BlobKey 對應的檔案傳送到使用者端。 // Blobstore 會自行判斷檔案類型,選擇適合的呈現方式。

61 資料庫 網路溝通 圖形處理 工作排程 其他服務 Google App Engine API 專題

62 Google App Engine API 專題
專題內容必須包含下列技術: Low-Level API / JDO 利用 Low-Level API 或 JDO 對資料庫進行 CRUD。 URLFetch 擷取其它網站提供的資訊或媒體,而後進行處理。 Cron 可以搭配 URLFetch,週期性的擷取資料。 Users 利用 Users 做為使用者認證方式。 可以考慮加入其它 API 應用 Mail、Image、Task Queue、Memcache


Download ppt "Google App Engine API."

Similar presentations


Ads by Google