1d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen/** 2d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * $Revision$ 3d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * $Date$ 4d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 5d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * Copyright 2003-2007 Jive Software. 6d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 7d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); 8d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * you may not use this file except in compliance with the License. 9d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * You may obtain a copy of the License at 10d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 11d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * http://www.apache.org/licenses/LICENSE-2.0 12d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 13d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * Unless required by applicable law or agreed to in writing, software 14d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * distributed under the License is distributed on an "AS IS" BASIS, 15d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * See the License for the specific language governing permissions and 17d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * limitations under the License. 18d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen */ 19d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 20d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chenpackage org.jivesoftware.smackx.bookmark; 21d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 22d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chenimport org.jivesoftware.smack.Connection; 23d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chenimport org.jivesoftware.smack.XMPPException; 24d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chenimport org.jivesoftware.smackx.PrivateDataManager; 25d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 26d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chenimport java.util.*; 27d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 28d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen/** 29d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * Provides methods to manage bookmarks in accordance with JEP-0048. Methods for managing URLs and 30d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * Conferences are provided. 31d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * </p> 32d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * It should be noted that some extensions have been made to the JEP. There is an attribute on URLs 33d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * that marks a url as a news feed and also a sub-element can be added to either a URL or conference 34d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * indicated that it is shared amongst all users on a server. 35d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 36d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @author Alexander Wenckus 37d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen */ 38d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chenpublic class BookmarkManager { 39d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen private static final Map<Connection, BookmarkManager> bookmarkManagerMap = new HashMap<Connection, BookmarkManager>(); 40d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen static { 41d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen PrivateDataManager.addPrivateDataProvider("storage", "storage:bookmarks", 42d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen new Bookmarks.Provider()); 43d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 44d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 45d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen /** 46d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * Returns the <i>BookmarkManager</i> for a connection, if it doesn't exist it is created. 47d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 48d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @param connection the connection for which the manager is desired. 49d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @return Returns the <i>BookmarkManager</i> for a connection, if it doesn't 50d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * exist it is created. 51d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @throws XMPPException Thrown if the connection is null or has not yet been authenticated. 52d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen */ 53d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen public synchronized static BookmarkManager getBookmarkManager(Connection connection) 54d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen throws XMPPException 55d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen { 56d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen BookmarkManager manager = (BookmarkManager) bookmarkManagerMap.get(connection); 57d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen if(manager == null) { 58d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen manager = new BookmarkManager(connection); 59d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen bookmarkManagerMap.put(connection, manager); 60d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 61d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen return manager; 62d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 63d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 64d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen private PrivateDataManager privateDataManager; 65d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen private Bookmarks bookmarks; 66d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen private final Object bookmarkLock = new Object(); 67d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 68d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen /** 69d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * Default constructor. Registers the data provider with the private data manager in the 70d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * storage:bookmarks namespace. 71d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 72d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @param connection the connection for persisting and retrieving bookmarks. 73d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @throws XMPPException thrown when the connection is null or has not been authenticated. 74d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen */ 75d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen private BookmarkManager(Connection connection) throws XMPPException { 76d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen if(connection == null || !connection.isAuthenticated()) { 77d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen throw new XMPPException("Invalid connection."); 78d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 79d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen this.privateDataManager = new PrivateDataManager(connection); 80d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 81d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 82d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen /** 83d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * Returns all currently bookmarked conferences. 84d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 85d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @return returns all currently bookmarked conferences 86d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @throws XMPPException thrown when there was an error retrieving the current bookmarks from 87d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * the server. 88d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @see BookmarkedConference 89d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen */ 90d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen public Collection<BookmarkedConference> getBookmarkedConferences() throws XMPPException { 91d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen retrieveBookmarks(); 92d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen return Collections.unmodifiableCollection(bookmarks.getBookmarkedConferences()); 93d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 94d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 95d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen /** 96d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * Adds or updates a conference in the bookmarks. 97d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 98d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @param name the name of the conference 99d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @param jid the jid of the conference 100d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @param isAutoJoin whether or not to join this conference automatically on login 101d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @param nickname the nickname to use for the user when joining the conference 102d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @param password the password to use for the user when joining the conference 103d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @throws XMPPException thrown when there is an issue retrieving the current bookmarks from 104d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * the server. 105d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen */ 106d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen public void addBookmarkedConference(String name, String jid, boolean isAutoJoin, 107d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen String nickname, String password) throws XMPPException 108d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen { 109d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen retrieveBookmarks(); 110d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen BookmarkedConference bookmark 111d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen = new BookmarkedConference(name, jid, isAutoJoin, nickname, password); 112d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen List<BookmarkedConference> conferences = bookmarks.getBookmarkedConferences(); 113d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen if(conferences.contains(bookmark)) { 114d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen BookmarkedConference oldConference = conferences.get(conferences.indexOf(bookmark)); 115d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen if(oldConference.isShared()) { 116d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen throw new IllegalArgumentException("Cannot modify shared bookmark"); 117d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 118d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen oldConference.setAutoJoin(isAutoJoin); 119d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen oldConference.setName(name); 120d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen oldConference.setNickname(nickname); 121d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen oldConference.setPassword(password); 122d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 123d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen else { 124d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen bookmarks.addBookmarkedConference(bookmark); 125d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 126d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen privateDataManager.setPrivateData(bookmarks); 127d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 128d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 129d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen /** 130d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * Removes a conference from the bookmarks. 131d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 132d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @param jid the jid of the conference to be removed. 133d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @throws XMPPException thrown when there is a problem with the connection attempting to 134d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * retrieve the bookmarks or persist the bookmarks. 135d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @throws IllegalArgumentException thrown when the conference being removed is a shared 136d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * conference 137d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen */ 138d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen public void removeBookmarkedConference(String jid) throws XMPPException { 139d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen retrieveBookmarks(); 140d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen Iterator<BookmarkedConference> it = bookmarks.getBookmarkedConferences().iterator(); 141d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen while(it.hasNext()) { 142d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen BookmarkedConference conference = it.next(); 143d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen if(conference.getJid().equalsIgnoreCase(jid)) { 144d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen if(conference.isShared()) { 145d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen throw new IllegalArgumentException("Conference is shared and can't be removed"); 146d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 147d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen it.remove(); 148d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen privateDataManager.setPrivateData(bookmarks); 149d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen return; 150d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 151d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 152d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 153d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 154d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen /** 155d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * Returns an unmodifiable collection of all bookmarked urls. 156d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 157d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @return returns an unmodifiable collection of all bookmarked urls. 158d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @throws XMPPException thrown when there is a problem retriving bookmarks from the server. 159d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen */ 160d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen public Collection<BookmarkedURL> getBookmarkedURLs() throws XMPPException { 161d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen retrieveBookmarks(); 162d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen return Collections.unmodifiableCollection(bookmarks.getBookmarkedURLS()); 163d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 164d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 165d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen /** 166d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * Adds a new url or updates an already existing url in the bookmarks. 167d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 168d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @param URL the url of the bookmark 169d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @param name the name of the bookmark 170d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @param isRSS whether or not the url is an rss feed 171d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @throws XMPPException thrown when there is an error retriving or saving bookmarks from or to 172d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * the server 173d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen */ 174d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen public void addBookmarkedURL(String URL, String name, boolean isRSS) throws XMPPException { 175d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen retrieveBookmarks(); 176d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen BookmarkedURL bookmark = new BookmarkedURL(URL, name, isRSS); 177d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen List<BookmarkedURL> urls = bookmarks.getBookmarkedURLS(); 178d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen if(urls.contains(bookmark)) { 179d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen BookmarkedURL oldURL = urls.get(urls.indexOf(bookmark)); 180d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen if(oldURL.isShared()) { 181d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen throw new IllegalArgumentException("Cannot modify shared bookmarks"); 182d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 183d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen oldURL.setName(name); 184d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen oldURL.setRss(isRSS); 185d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 186d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen else { 187d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen bookmarks.addBookmarkedURL(bookmark); 188d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 189d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen privateDataManager.setPrivateData(bookmarks); 190d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 191d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 192d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen /** 193d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * Removes a url from the bookmarks. 194d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * 195d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @param bookmarkURL the url of the bookmark to remove 196d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * @throws XMPPException thrown if there is an error retriving or saving bookmarks from or to 197d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen * the server. 198d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen */ 199d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen public void removeBookmarkedURL(String bookmarkURL) throws XMPPException { 200d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen retrieveBookmarks(); 201d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen Iterator<BookmarkedURL> it = bookmarks.getBookmarkedURLS().iterator(); 202d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen while(it.hasNext()) { 203d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen BookmarkedURL bookmark = it.next(); 204d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen if(bookmark.getURL().equalsIgnoreCase(bookmarkURL)) { 205d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen if(bookmark.isShared()) { 206d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen throw new IllegalArgumentException("Cannot delete a shared bookmark."); 207d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 208d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen it.remove(); 209d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen privateDataManager.setPrivateData(bookmarks); 210d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen return; 211d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 212d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 213d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 214d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen 215d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen private Bookmarks retrieveBookmarks() throws XMPPException { 216d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen synchronized(bookmarkLock) { 217d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen if(bookmarks == null) { 218d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen bookmarks = (Bookmarks) privateDataManager.getPrivateData("storage", 219d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen "storage:bookmarks"); 220d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 221d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen return bookmarks; 222d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 223d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen } 224d7955ce24d294fb2014c59d11fca184471056f44Shuyi Chen} 225