Java tutorial
/* * Copyright 2015 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package collection; import org.apache.commons.codec.binary.Base64; import org.json.JSONArray; import org.json.JSONObject; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** * Collection of Physical Web URL devices and related metadata. */ public class PhysicalWebCollection { private static final int SCHEMA_VERSION = 1; private static final String SCHEMA_VERSION_KEY = "schema"; private static final String DEVICES_KEY = "devices"; private static final String METADATA_KEY = "metadata"; private static final String ICON_MAP_KEY = "iconmap"; private PwsClient mPwsClient; private Map<String, UrlDevice> mDeviceIdToUrlDeviceMap; private Map<String, PwsResult> mBroadcastUrlToPwsResultMap; private Map<String, byte[]> mIconUrlToIconMap; private Set<String> mPendingBroadcastUrls; private Set<String> mPendingIconUrls; private Set<String> mFailedResolveUrls; /** * Construct a PhysicalWebCollection. */ public PhysicalWebCollection() { mPwsClient = new PwsClient(); mDeviceIdToUrlDeviceMap = new HashMap<>(); mBroadcastUrlToPwsResultMap = new HashMap<>(); mIconUrlToIconMap = new HashMap<>(); mPendingBroadcastUrls = new HashSet<>(); mPendingIconUrls = new HashSet<>(); mFailedResolveUrls = new HashSet<>(); } /** * Add a UrlDevice to the collection. * @param urlDevice The UrlDevice to add. * @return true if the device already existed in the map */ public boolean addUrlDevice(UrlDevice urlDevice) { boolean alreadyFound = mDeviceIdToUrlDeviceMap.containsKey(urlDevice.getId()); mDeviceIdToUrlDeviceMap.put(urlDevice.getId(), urlDevice); return alreadyFound; } /** * Remove a UrlDevice from the collection. * @param urlDevice The UrlDevice to remove. */ public void removeUrlDevice(UrlDevice urlDevice) { mDeviceIdToUrlDeviceMap.remove(urlDevice.getId()); } /** * Add URL metadata to the collection. * @param pwsResult The PwsResult to add. */ public void addMetadata(PwsResult pwsResult) { mBroadcastUrlToPwsResultMap.put(pwsResult.getRequestUrl(), pwsResult); } /** * Add an Icon to the collection. * @param url The url of the icon. * @param icon The bitmap of the icon. */ public void addIcon(String url, byte[] icon) { mIconUrlToIconMap.put(url, icon); } /** * Clear results and devices. */ public void clear() { mDeviceIdToUrlDeviceMap.clear(); mBroadcastUrlToPwsResultMap.clear(); mIconUrlToIconMap.clear(); mPendingBroadcastUrls.clear(); mPendingIconUrls.clear(); mFailedResolveUrls.clear(); } /** * Get an Icon from the collection. * @param url The url of the icon. * @return The associated icon. This will be null if there is no icon. */ public byte[] getIcon(String url) { return mIconUrlToIconMap.get(url); } /** * Fetches a UrlDevice by its ID. * @param id The ID of the UrlDevice. * @return the UrlDevice with the given ID. */ public UrlDevice getUrlDeviceById(String id) { return mDeviceIdToUrlDeviceMap.get(id); } /** * Fetches cached URL metadata using the URL broadcasted by the Physical Web device. * @param broadcastUrl The URL broadcasted by the device. * @return Cached metadata relevant to the given URL. */ public PwsResult getMetadataByBroadcastUrl(String broadcastUrl) { return mBroadcastUrlToPwsResultMap.get(broadcastUrl); } /** * Gets all UrlDevices stored in the collection. * @return List of UrlDevices */ public List<UrlDevice> getUrlDevices() { return (List) new ArrayList(mDeviceIdToUrlDeviceMap.values()); } /** * Create a JSON object that represents this data structure. * @return a JSON serialization of this data structure. */ public JSONObject jsonSerialize() { JSONObject jsonObject = new JSONObject(); // Serialize the UrlDevices JSONArray urlDevices = new JSONArray(); for (UrlDevice urlDevice : mDeviceIdToUrlDeviceMap.values()) { urlDevices.put(urlDevice.jsonSerialize()); } jsonObject.put(DEVICES_KEY, urlDevices); // Serialize the URL metadata JSONArray metadata = new JSONArray(); for (PwsResult pwsResult : mBroadcastUrlToPwsResultMap.values()) { metadata.put(pwsResult.jsonSerialize()); } jsonObject.put(METADATA_KEY, metadata); JSONObject iconMap = new JSONObject(); for (String iconUrl : mIconUrlToIconMap.keySet()) { iconMap.put(iconUrl, new String(Base64.encodeBase64(getIcon(iconUrl)), Charset.forName("UTF-8"))); } jsonObject.put(ICON_MAP_KEY, iconMap); jsonObject.put(SCHEMA_VERSION_KEY, SCHEMA_VERSION); return jsonObject; } /** * Populate this data structure with UrlDevices represented by a given JSON object. * @param jsonObject a serialized PhysicalWebCollection. * @return The PhysicalWebCollection represented by the serialized object. * @throws PhysicalWebCollectionException on invalid or unrecognized input */ public static PhysicalWebCollection jsonDeserialize(JSONObject jsonObject) throws PhysicalWebCollectionException { // Check the schema version int schemaVersion = jsonObject.getInt(SCHEMA_VERSION_KEY); if (schemaVersion > SCHEMA_VERSION) { throw new PhysicalWebCollectionException("Cannot handle schema version " + schemaVersion + ". " + "This library only knows of schema version " + SCHEMA_VERSION); } PhysicalWebCollection collection = new PhysicalWebCollection(); // Deserialize the UrlDevices JSONArray urlDevices = jsonObject.getJSONArray(DEVICES_KEY); for (int i = 0; i < urlDevices.length(); i++) { JSONObject urlDeviceJson = urlDevices.getJSONObject(i); UrlDevice urlDevice = UrlDevice.jsonDeserialize(urlDeviceJson); collection.addUrlDevice(urlDevice); } // Deserialize the URL metadata JSONArray metadata = jsonObject.getJSONArray(METADATA_KEY); for (int i = 0; i < metadata.length(); i++) { JSONObject pwsResultJson = metadata.getJSONObject(i); PwsResult pwsResult = PwsResult.jsonDeserialize(pwsResultJson); collection.addMetadata(pwsResult); } JSONObject iconMap = jsonObject.getJSONObject(ICON_MAP_KEY); for (Iterator<String> iconUrls = iconMap.keys(); iconUrls.hasNext();) { String iconUrl = iconUrls.next(); collection.addIcon(iconUrl, Base64.decodeBase64(iconMap.getString(iconUrl).getBytes(Charset.forName("UTF-8")))); } return collection; } /** * Return a list of PwPairs sorted by rank in descending order. * These PwPairs will be deduplicated by siteUrls (favoring the PwPair with * the highest rank). * @param comparator to sort pairs by * @return a sorted list of PwPairs. */ public List<PwPair> getPwPairsSortedByRank(Comparator<PwPair> comparator) { // Get all valid PwPairs. List<PwPair> allPwPairs = getPwPairs(); // Sort the list in descending order. Collections.sort(allPwPairs, comparator); // Filter the list. return removeDuplicateSiteUrls(allPwPairs); } /** * Return a list of PwPairs sorted by rank in descending order, including only the top-ranked * pair from each group. * @param comparator to sort pairs by * @return a sorted list of PwPairs. */ public List<PwPair> getGroupedPwPairsSortedByRank(Comparator<PwPair> comparator) { // Get all valid PwPairs. List<PwPair> allPwPairs = getPwPairs(); // Group pairs with the same groupId, keeping only the top-ranked PwPair. List<PwPair> groupedPwPairs = removeDuplicateGroupIds(allPwPairs, null, comparator); // Sort by descending rank. Collections.sort(groupedPwPairs, comparator); // Remove duplicate site URLs. return removeDuplicateSiteUrls(groupedPwPairs); } /** * Return a list of all pairs of valid URL devices and corresponding URL metadata. * @return list of PwPairs. */ public List<PwPair> getPwPairs() { List<PwPair> allPwPairs = new ArrayList<>(); for (UrlDevice urlDevice : mDeviceIdToUrlDeviceMap.values()) { PwsResult pwsResult = mBroadcastUrlToPwsResultMap.get(urlDevice.getUrl()); if (pwsResult != null) { allPwPairs.add(new PwPair(urlDevice, pwsResult)); } } return allPwPairs; } /** * Return the top-ranked PwPair for a given group ID. * @param groupId * @param comparator to sort pairs by * @return a PwPair. */ public PwPair getTopRankedPwPairByGroupId(String groupId, Comparator<PwPair> comparator) { for (PwPair pwPair : getGroupedPwPairsSortedByRank(comparator)) { if (pwPair.getPwsResult().getGroupId().equals(groupId)) { return pwPair; } } return null; } /** * If a site URL appears multiple times in the pairs list, keep only the first example. * @param allPwPairs input PwPairs list. * @return filtered PwPairs list with all duplicated site URLs removed. */ private static List<PwPair> removeDuplicateSiteUrls(List<PwPair> allPwPairs) { List<PwPair> filteredPwPairs = new ArrayList<>(); Set<String> siteUrls = new HashSet<>(); for (PwPair pwPair : allPwPairs) { String siteUrl = pwPair.getPwsResult().getSiteUrl(); if (!siteUrls.contains(siteUrl)) { siteUrls.add(siteUrl); filteredPwPairs.add(pwPair); } } return filteredPwPairs; } /** * Given a list of PwPairs, return a filtered list such that only one PwPair from each group * is included. * @param allPairs Input PwPairs list. * @param outGroupMap Optional output map from discovered group IDs to UrlGroups, may be null. * @return Filtered PwPairs list. */ private static List<PwPair> removeDuplicateGroupIds(List<PwPair> allPairs, Map<String, UrlGroup> outGroupMap, Comparator<PwPair> comparator) { List<PwPair> filteredPairs = new ArrayList<>(); Map<String, UrlGroup> groupMap = outGroupMap; if (groupMap == null) { groupMap = new HashMap<>(); } else { groupMap.clear(); } for (PwPair pwPair : allPairs) { PwsResult pwsResult = pwPair.getPwsResult(); String groupId = pwsResult.getGroupId(); if (groupId == null || groupId.equals("")) { // Pairs without a group are always included filteredPairs.add(pwPair); } else { // Create the group if it doesn't exist UrlGroup urlGroup = groupMap.get(groupId); if (urlGroup == null) { urlGroup = new UrlGroup(groupId); groupMap.put(groupId, urlGroup); } urlGroup.addPair(pwPair); } } for (UrlGroup urlGroup : groupMap.values()) { filteredPairs.add(urlGroup.getTopPair(comparator)); } return filteredPairs; } /** * Set the URL, the API version, the API Key for making PWS requests. * @param pwsEndpoint The new PWS endpoint. * @param pwsApiVersion The new PWS API version. */ public void setPwsEndpoint(String pwsEndpoint, int pwsApiVersion) { mPwsClient.setEndpoint(pwsEndpoint, pwsApiVersion); } /** * Set the URL, the API version, the API Key for making PWS requests. * @param pwsEndpoint The new PWS endpoint. * @param pwsApiVersion The new PWS API version. * @param pwsApiKey The new PWS API key. */ public void setPwsEndpoint(String pwsEndpoint, int pwsApiVersion, String pwsApiKey) { mPwsClient.setEndpoint(pwsEndpoint, pwsApiVersion, pwsApiKey); } private class AugmentedPwsResultIconCallback extends PwsResultIconCallback { private String mUrl; private PwsResultIconCallback mCallback; AugmentedPwsResultIconCallback(String url, PwsResultIconCallback callback) { mUrl = url; mCallback = callback; } @Override public void onIcon(byte[] icon) { mPendingIconUrls.remove(mUrl); addIcon(mUrl, icon); mCallback.onIcon(icon); } @Override public void onError(int httpResponseCode, Exception e) { mPendingIconUrls.remove(mUrl); mCallback.onError(httpResponseCode, e); } } /** * Triggers an HTTP request to be made to the PWS. * This method fetches a results from the PWS for all broadcast URLs, * depending on the supplied parameters. * @param pwsResultCallback The callback to run when we get an HTTPResponse. * If this value is null, we will not fetch the PwsResults, only icons. * @param pwsResultIconCallback The callback to run when we get a favicon. * If this value is null, we will not fetch the icons. */ public void fetchPwsResults(final PwsResultCallback pwsResultCallback, final PwsResultIconCallback pwsResultIconCallback) { // Get new URLs to fetch. Set<String> newResolveUrls = new HashSet<>(); Set<String> newIconUrls = new HashSet<>(); for (UrlDevice urlDevice : mDeviceIdToUrlDeviceMap.values()) { String url = urlDevice.getUrl(); if (!mPendingBroadcastUrls.contains(url) && !mFailedResolveUrls.contains(url)) { PwsResult pwsResult = mBroadcastUrlToPwsResultMap.get(url); if (pwsResult == null) { newResolveUrls.add(url); mPendingBroadcastUrls.add(url); } else if (pwsResult.hasIconUrl() && !mPendingIconUrls.contains(pwsResult.getIconUrl()) && !mIconUrlToIconMap.containsKey(pwsResult.getIconUrl())) { newIconUrls.add(pwsResult.getIconUrl()); mPendingIconUrls.add(pwsResult.getIconUrl()); } } } // Make the resolve request. final Set<String> finalResolveUrls = newResolveUrls; PwsResultCallback augmentedCallback = new PwsResultCallback() { @Override public void onPwsResult(PwsResult pwsResult) { addMetadata(pwsResult); if (pwsResultIconCallback != null) { PwsResultIconCallback augmentedIconCallback = new AugmentedPwsResultIconCallback( pwsResult.getIconUrl(), pwsResultIconCallback); mPwsClient.downloadIcon(pwsResult.getIconUrl(), augmentedIconCallback); } pwsResultCallback.onPwsResult(pwsResult); } @Override public void onPwsResultAbsent(String url) { mFailedResolveUrls.add(url); pwsResultCallback.onPwsResultAbsent(url); } @Override public void onPwsResultError(Collection<String> urls, int httpResponseCode, Exception e) { pwsResultCallback.onPwsResultError(urls, httpResponseCode, e); } @Override public void onResponseReceived(long durationMillis) { for (String url : finalResolveUrls) { mPendingBroadcastUrls.remove(url); } pwsResultCallback.onResponseReceived(durationMillis); } }; if (pwsResultCallback != null && newResolveUrls.size() > 0) { mPwsClient.resolve(newResolveUrls, augmentedCallback); } // Make the icon requests. if (pwsResultIconCallback != null) { for (final String iconUrl : newIconUrls) { PwsResultIconCallback augmentedIconCallback = new AugmentedPwsResultIconCallback(iconUrl, pwsResultIconCallback); mPwsClient.downloadIcon(iconUrl, augmentedIconCallback); } } } /** * Cancel all current HTTP requests. */ public void cancelAllRequests() { mPwsClient.cancelAllRequests(); } }