PageRenderTime 47ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/connector/src/main/java/com/restfb/DefaultFacebookClient.java

https://github.com/chrbayer84/GoodData-CL
Java | 592 lines | 276 code | 87 blank | 229 comment | 44 complexity | 9249fcf382f5f32370d2dd3679e87e5d MD5 | raw file
Possible License(s): BSD-3-Clause
  1. /*
  2. * Copyright (c) 2010-2011 Mark Allen.
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20. * THE SOFTWARE.
  21. */
  22. package com.restfb;
  23. import com.restfb.WebRequestor.Response;
  24. import com.restfb.exception.*;
  25. import com.restfb.json.JsonArray;
  26. import com.restfb.json.JsonException;
  27. import com.restfb.json.JsonObject;
  28. import java.io.IOException;
  29. import java.io.InputStream;
  30. import java.util.ArrayList;
  31. import java.util.Arrays;
  32. import java.util.List;
  33. import java.util.Map;
  34. import static com.restfb.util.StringUtils.*;
  35. import static java.net.HttpURLConnection.*;
  36. import static java.util.Collections.emptyList;
  37. import static java.util.logging.Level.INFO;
  38. /**
  39. * Default implementation of a <a
  40. * href="http://developers.facebook.com/docs/api">Facebook Graph API</a> client.
  41. *
  42. * @author <a href="http://restfb.com">Mark Allen</a>
  43. */
  44. public class DefaultFacebookClient extends BaseFacebookClient implements FacebookClient {
  45. /**
  46. * Graph API access token.
  47. */
  48. protected String accessToken;
  49. /**
  50. * Knows how to map Graph API exceptions to formal Java exception types.
  51. */
  52. protected FacebookGraphExceptionMapper facebookGraphExceptionMapper;
  53. /**
  54. * API endpoint URL.
  55. */
  56. protected static final String FACEBOOK_GRAPH_ENDPOINT_URL = "https://graph.facebook.com";
  57. /**
  58. * Legacy API endpoint URL, used to support FQL queries.
  59. */
  60. protected static final String FACEBOOK_LEGACY_ENDPOINT_URL = "https://api.facebook.com/method";
  61. /**
  62. * Reserved method override parameter name.
  63. */
  64. protected static final String METHOD_PARAM_NAME = "method";
  65. /**
  66. * Reserved "multiple IDs" parameter name.
  67. */
  68. protected static final String IDS_PARAM_NAME = "ids";
  69. /**
  70. * Reserved FQL query parameter name.
  71. */
  72. protected static final String QUERY_PARAM_NAME = "query";
  73. /**
  74. * Reserved FQL multiquery parameter name.
  75. */
  76. protected static final String QUERIES_PARAM_NAME = "queries";
  77. /**
  78. * Reserved "result format" parameter name.
  79. */
  80. protected static final String FORMAT_PARAM_NAME = "format";
  81. /**
  82. * API error response 'error' attribute name.
  83. */
  84. protected static final String ERROR_ATTRIBUTE_NAME = "error";
  85. /**
  86. * API error response 'type' attribute name.
  87. */
  88. protected static final String ERROR_TYPE_ATTRIBUTE_NAME = "type";
  89. /**
  90. * API error response 'message' attribute name.
  91. */
  92. protected static final String ERROR_MESSAGE_ATTRIBUTE_NAME = "message";
  93. /**
  94. * Creates a Facebook Graph API client with no access token.
  95. * <p/>
  96. * Without an access token, you can view and search public graph data but
  97. * can't do much else.
  98. */
  99. public DefaultFacebookClient() {
  100. this(null);
  101. }
  102. /**
  103. * Creates a Facebook Graph API client with the given {@code accessToken}.
  104. *
  105. * @param accessToken A Facebook OAuth access token.
  106. */
  107. public DefaultFacebookClient(String accessToken) {
  108. this(accessToken, new DefaultWebRequestor(), new DefaultJsonMapper());
  109. }
  110. /**
  111. * Creates a Facebook Graph API client with the given {@code accessToken},
  112. * {@code webRequestor}, and {@code jsonMapper}.
  113. *
  114. * @param accessToken A Facebook OAuth access token.
  115. * @param webRequestor The {@link WebRequestor} implementation to use for sending
  116. * requests to the API endpoint.
  117. * @param jsonMapper The {@link JsonMapper} implementation to use for mapping API
  118. * response JSON to Java objects.
  119. * @throws NullPointerException If {@code jsonMapper} or {@code webRequestor} is {@code null}.
  120. */
  121. public DefaultFacebookClient(String accessToken, WebRequestor webRequestor, JsonMapper jsonMapper) {
  122. verifyParameterPresence("jsonMapper", jsonMapper);
  123. verifyParameterPresence("webRequestor", webRequestor);
  124. this.accessToken = trimToNull(accessToken);
  125. this.webRequestor = webRequestor;
  126. this.jsonMapper = jsonMapper;
  127. this.facebookGraphExceptionMapper = createFacebookGraphExceptionMapper();
  128. illegalParamNames.addAll(Arrays
  129. .asList(new String[]{ACCESS_TOKEN_PARAM_NAME, METHOD_PARAM_NAME, FORMAT_PARAM_NAME}));
  130. }
  131. /**
  132. * @see com.restfb.FacebookClient#deleteObject(String)
  133. */
  134. @Override
  135. public boolean deleteObject(String object) {
  136. verifyParameterPresence("object", object);
  137. return "true".equals(makeRequest(object, false, true, true, null));
  138. }
  139. /**
  140. * @see com.restfb.FacebookClient#fetchConnection(String,
  141. * Class, com.restfb.Parameter[])
  142. */
  143. @Override
  144. public <T> Connection<T> fetchConnection(String connection, Class<T> connectionType, Parameter... parameters) {
  145. verifyParameterPresence("connection", connection);
  146. verifyParameterPresence("connectionType", connectionType);
  147. return mapToConnection(makeRequest(connection, parameters), connectionType);
  148. }
  149. /**
  150. * @see com.restfb.FacebookClient#fetchConnectionPage(String,
  151. * Class)
  152. */
  153. @Override
  154. public <T> Connection<T> fetchConnectionPage(final String connectionPageUrl, Class<T> connectionType) {
  155. String connectionJson = makeRequestAndProcessResponse(new Requestor() {
  156. /**
  157. * @see com.restfb.DefaultFacebookClient.Requestor#makeRequest()
  158. */
  159. @Override
  160. public Response makeRequest() throws IOException {
  161. return webRequestor.executeGet(connectionPageUrl);
  162. }
  163. });
  164. return mapToConnection(connectionJson, connectionType);
  165. }
  166. @SuppressWarnings("unchecked")
  167. protected <T> Connection<T> mapToConnection(String connectionJson, Class<T> connectionType) {
  168. List<T> data = new ArrayList<T>();
  169. String previous = null;
  170. String next = null;
  171. try {
  172. JsonObject jsonObject = new JsonObject(connectionJson);
  173. // Pull out data
  174. JsonArray jsonData = jsonObject.getJsonArray("data");
  175. for (int i = 0; i < jsonData.length(); i++)
  176. data.add(connectionType.equals(JsonObject.class) ? (T) jsonData.get(i) : jsonMapper.toJavaObject(jsonData
  177. .get(i).toString(), connectionType));
  178. // Pull out paging info, if present
  179. if (jsonObject.has("paging")) {
  180. JsonObject jsonPaging = jsonObject.getJsonObject("paging");
  181. previous = jsonPaging.has("previous") ? jsonPaging.getString("previous") : null;
  182. next = jsonPaging.has("next") ? jsonPaging.getString("next") : null;
  183. }
  184. } catch (JsonException e) {
  185. throw new FacebookJsonMappingException("Unable to map connection JSON to Java objects", e);
  186. }
  187. return new Connection<T>(data, previous, next);
  188. }
  189. /**
  190. * @see com.restfb.FacebookClient#fetchObject(String,
  191. * Class, com.restfb.Parameter[])
  192. */
  193. @Override
  194. public <T> T fetchObject(String object, Class<T> objectType, Parameter... parameters) {
  195. verifyParameterPresence("object", object);
  196. verifyParameterPresence("objectType", objectType);
  197. return jsonMapper.toJavaObject(makeRequest(object, parameters), objectType);
  198. }
  199. /**
  200. * @see com.restfb.FacebookClient#fetchObjects(java.util.List,
  201. * Class, com.restfb.Parameter[])
  202. */
  203. @Override
  204. @SuppressWarnings("unchecked")
  205. public <T> T fetchObjects(List<String> ids, Class<T> objectType, Parameter... parameters) {
  206. verifyParameterPresence("ids", ids);
  207. verifyParameterPresence("connectionType", objectType);
  208. if (ids.size() == 0)
  209. throw new IllegalArgumentException("The list of IDs cannot be empty.");
  210. for (Parameter parameter : parameters)
  211. if (IDS_PARAM_NAME.equals(parameter.name))
  212. throw new IllegalArgumentException("You cannot specify the '" + IDS_PARAM_NAME + "' URL parameter yourself - "
  213. + "RestFB will populate this for you with " + "the list of IDs you passed to this method.");
  214. // Normalize the IDs
  215. for (int i = 0; i < ids.size(); i++) {
  216. String id = ids.get(i).trim().toLowerCase();
  217. if ("".equals(id))
  218. throw new IllegalArgumentException("The list of IDs cannot contain blank strings.");
  219. ids.set(i, id);
  220. }
  221. try {
  222. JsonObject jsonObject =
  223. new JsonObject(makeRequest("",
  224. parametersWithAdditionalParameter(Parameter.with(IDS_PARAM_NAME, join(ids)), parameters)));
  225. return objectType.equals(JsonObject.class) ? (T) jsonObject : jsonMapper.toJavaObject(jsonObject.toString(),
  226. objectType);
  227. } catch (JsonException e) {
  228. throw new FacebookJsonMappingException("Unable to map connection JSON to Java objects", e);
  229. }
  230. }
  231. /**
  232. * @see com.restfb.FacebookClient#publish(String, Class,
  233. * java.io.InputStream, com.restfb.Parameter[])
  234. */
  235. @Override
  236. public <T> T publish(String connection, Class<T> objectType, InputStream binaryAttachment, Parameter... parameters) {
  237. verifyParameterPresence("connection", connection);
  238. return jsonMapper.toJavaObject(makeRequest(connection, false, true, false, binaryAttachment, parameters),
  239. objectType);
  240. }
  241. /**
  242. * @see com.restfb.FacebookClient#publish(String, Class,
  243. * com.restfb.Parameter[])
  244. */
  245. @Override
  246. public <T> T publish(String connection, Class<T> objectType, Parameter... parameters) {
  247. return publish(connection, objectType, null, parameters);
  248. }
  249. /**
  250. * @see com.restfb.FacebookClient#executeMultiquery(java.util.Map,
  251. * Class, com.restfb.Parameter[])
  252. */
  253. @Override
  254. @SuppressWarnings("unchecked")
  255. public <T> T executeMultiquery(Map<String, String> queries, Class<T> objectType, Parameter... parameters) {
  256. verifyParameterPresence("objectType", objectType);
  257. for (Parameter parameter : parameters)
  258. if (QUERIES_PARAM_NAME.equals(parameter.name))
  259. throw new IllegalArgumentException("You cannot specify the '" + QUERIES_PARAM_NAME
  260. + "' URL parameter yourself - " + "RestFB will populate this for you with "
  261. + "the queries you passed to this method.");
  262. try {
  263. JsonArray jsonArray =
  264. new JsonArray(makeRequest("fql.multiquery", true, false, false, null,
  265. parametersWithAdditionalParameter(Parameter.with(QUERIES_PARAM_NAME, queriesToJson(queries)), parameters)));
  266. JsonObject normalizedJson = new JsonObject();
  267. for (int i = 0; i < jsonArray.length(); i++) {
  268. JsonObject jsonObject = jsonArray.getJsonObject(i);
  269. // For empty resultsets, Facebook will return an empty object instead of
  270. // an empty list. Hack around that here.
  271. JsonArray resultsArray =
  272. jsonObject.get("fql_result_set") instanceof JsonArray ? jsonObject.getJsonArray("fql_result_set")
  273. : new JsonArray();
  274. normalizedJson.put(jsonObject.getString("name"), resultsArray);
  275. }
  276. return objectType.equals(JsonObject.class) ? (T) normalizedJson : jsonMapper.toJavaObject(
  277. normalizedJson.toString(), objectType);
  278. } catch (JsonException e) {
  279. throw new FacebookJsonMappingException("Unable to process fql.multiquery JSON response", e);
  280. }
  281. }
  282. /**
  283. * @see com.restfb.FacebookClient#executeQuery(String,
  284. * Class, com.restfb.Parameter[])
  285. */
  286. @Override
  287. public <T> List<T> executeQuery(String query, Class<T> objectType, Parameter... parameters) {
  288. verifyParameterPresence("query", query);
  289. verifyParameterPresence("objectType", objectType);
  290. for (Parameter parameter : parameters)
  291. if (QUERY_PARAM_NAME.equals(parameter.name))
  292. throw new IllegalArgumentException("You cannot specify the '" + QUERY_PARAM_NAME
  293. + "' URL parameter yourself - " + "RestFB will populate this for you with "
  294. + "the query you passed to this method.");
  295. return jsonMapper.toJavaList(
  296. makeRequest("fql.query", true, false, false, null,
  297. parametersWithAdditionalParameter(Parameter.with(QUERY_PARAM_NAME, query), parameters)), objectType);
  298. }
  299. /**
  300. * @see com.restfb.FacebookClient#convertSessionKeysToAccessTokens(String,
  301. * String, String[])
  302. */
  303. @Override
  304. public List<AccessToken> convertSessionKeysToAccessTokens(String appId, String secretKey, String... sessionKeys) {
  305. verifyParameterPresence("appId", appId);
  306. verifyParameterPresence("secretKey", secretKey);
  307. if (sessionKeys == null || sessionKeys.length == 0)
  308. return emptyList();
  309. String json =
  310. makeRequest("/oauth/exchange_sessions", false, true, false, null, Parameter.with("client_id", appId),
  311. Parameter.with("client_secret", secretKey), Parameter.with("sessions", join(sessionKeys)));
  312. return jsonMapper.toJavaList(json, AccessToken.class);
  313. }
  314. /**
  315. * Coordinates the process of executing the API request GET/POST and
  316. * processing the response we receive from the endpoint.
  317. *
  318. * @param endpoint Facebook Graph API endpoint.
  319. * @param parameters Arbitrary number of parameters to send along to Facebook as part
  320. * of the API call.
  321. * @return The JSON returned by Facebook for the API call.
  322. * @throws FacebookException If an error occurs while making the Facebook API POST or
  323. * processing the response.
  324. */
  325. protected String makeRequest(String endpoint, Parameter... parameters) {
  326. return makeRequest(endpoint, false, false, false, null, parameters);
  327. }
  328. /**
  329. * Coordinates the process of executing the API request GET/POST and
  330. * processing the response we receive from the endpoint.
  331. *
  332. * @param endpoint Facebook Graph API endpoint.
  333. * @param useLegacyEndpoint Should we hit the legacy endpoint ({@code true}) or the new Graph
  334. * endpoint ({@code false})?
  335. * @param executeAsPost {@code true} to execute the web request as a {@code POST},
  336. * {@code false} to execute as a {@code GET}.
  337. * @param executeAsDelete {@code true} to add a special 'treat this request as a
  338. * {@code DELETE}' parameter.
  339. * @param binaryAttachment A binary file to include in a {@code POST} request. Pass
  340. * {@code null} if no attachment should be sent.
  341. * @param parameters Arbitrary number of parameters to send along to Facebook as part
  342. * of the API call.
  343. * @return The JSON returned by Facebook for the API call.
  344. * @throws FacebookException If an error occurs while making the Facebook API POST or
  345. * processing the response.
  346. */
  347. protected String makeRequest(String endpoint, boolean useLegacyEndpoint, final boolean executeAsPost,
  348. boolean executeAsDelete, final InputStream binaryAttachment, Parameter... parameters) {
  349. verifyParameterLegality(parameters);
  350. if (executeAsDelete)
  351. parameters = parametersWithAdditionalParameter(Parameter.with(METHOD_PARAM_NAME, "delete"), parameters);
  352. trimToEmpty(endpoint).toLowerCase();
  353. if (!endpoint.startsWith("/"))
  354. endpoint = "/" + endpoint;
  355. final String fullEndpoint =
  356. (useLegacyEndpoint ? getFacebookLegacyEndpointUrl() : getFacebookGraphEndpointUrl()) + endpoint;
  357. final String parameterString = toParameterString(parameters);
  358. return makeRequestAndProcessResponse(new Requestor() {
  359. /**
  360. * @see com.restfb.DefaultFacebookClient.Requestor#makeRequest()
  361. */
  362. @Override
  363. public Response makeRequest() throws IOException {
  364. return executeAsPost ? webRequestor.executePost(fullEndpoint, parameterString, binaryAttachment) : webRequestor
  365. .executeGet(fullEndpoint + "?" + parameterString);
  366. }
  367. });
  368. }
  369. protected static interface Requestor {
  370. Response makeRequest() throws IOException;
  371. }
  372. protected String makeRequestAndProcessResponse(Requestor requestor) {
  373. Response response = null;
  374. // Perform a GET or POST to the API endpoint
  375. try {
  376. response = requestor.makeRequest();
  377. } catch (Throwable t) {
  378. throw new FacebookNetworkException("Facebook request failed", t);
  379. }
  380. if (logger.isLoggable(INFO))
  381. logger.info("Facebook responded with " + response);
  382. // If we get any HTTP response code other than a 200 OK or 400 Bad Request
  383. // or 401 Not Authorized or 403 Forbidden or 500 Internal Server Error,
  384. // throw an exception.
  385. if (HTTP_OK != response.getStatusCode() && HTTP_BAD_REQUEST != response.getStatusCode()
  386. && HTTP_UNAUTHORIZED != response.getStatusCode() && HTTP_INTERNAL_ERROR != response.getStatusCode()
  387. && HTTP_FORBIDDEN != response.getStatusCode())
  388. throw new FacebookNetworkException("Facebook request failed", response.getStatusCode());
  389. String json = response.getBody();
  390. // If the response contained an error code, throw an exception.
  391. throwFacebookResponseStatusExceptionIfNecessary(json);
  392. // If there was no response error information and this was a 500 or 401
  393. // error, something weird happened on Facebook's end. Bail.
  394. if (HTTP_INTERNAL_ERROR == response.getStatusCode() || HTTP_UNAUTHORIZED == response.getStatusCode())
  395. throw new FacebookNetworkException("Facebook request failed", response.getStatusCode());
  396. return json;
  397. }
  398. /**
  399. * Throws an exception if Facebook returned an error response. Using the Graph
  400. * API, it's possible to see both the new Graph API-style errors as well as
  401. * Legacy API-style errors, so we have to handle both here. This method
  402. * extracts relevant information from the error JSON and throws an exception
  403. * which encapsulates it for end-user consumption.
  404. * <p/>
  405. * For Graph API errors:
  406. * <p/>
  407. * If the {@code error} JSON field is present, we've got a response status
  408. * error for this API call.
  409. * <p/>
  410. * For Legacy errors (e.g. FQL):
  411. * <p/>
  412. * If the {@code error_code} JSON field is present, we've got a response
  413. * status error for this API call.
  414. *
  415. * @param json The JSON returned by Facebook in response to an API call.
  416. * @throws FacebookGraphException If the JSON contains a Graph API error response.
  417. * @throws FacebookResponseStatusException
  418. * If the JSON contains an Legacy API error response.
  419. * @throws FacebookJsonMappingException If an error occurs while processing the JSON.
  420. */
  421. protected void throwFacebookResponseStatusExceptionIfNecessary(String json) {
  422. // If we have a legacy exception, throw it.
  423. throwLegacyFacebookResponseStatusExceptionIfNecessary(json);
  424. try {
  425. // If the result is not an object, bail immediately.
  426. if (!json.startsWith("{"))
  427. return;
  428. JsonObject errorObject = new JsonObject(json);
  429. if (errorObject == null || !errorObject.has(ERROR_ATTRIBUTE_NAME))
  430. return;
  431. JsonObject innerErrorObject = errorObject.getJsonObject(ERROR_ATTRIBUTE_NAME);
  432. throw facebookGraphExceptionMapper
  433. .exceptionForTypeAndMessage(innerErrorObject.getString(ERROR_TYPE_ATTRIBUTE_NAME),
  434. innerErrorObject.getString(ERROR_MESSAGE_ATTRIBUTE_NAME));
  435. } catch (JsonException e) {
  436. throw new FacebookJsonMappingException("Unable to process the Facebook API response", e);
  437. }
  438. }
  439. /**
  440. * Specifies how we map Graph API exception types/messages to real Java
  441. * exceptions.
  442. * <p/>
  443. * Thanks to BatchFB's Jeff Schnitzer for doing some of the legwork to find
  444. * these exception type names.
  445. *
  446. * @return An instance of the exception mapper we should use.
  447. */
  448. protected FacebookGraphExceptionMapper createFacebookGraphExceptionMapper() {
  449. return new FacebookGraphExceptionMapper() {
  450. /**
  451. * @see com.restfb.exception.FacebookGraphExceptionMapper#exceptionForTypeAndMessage(String,
  452. * String)
  453. */
  454. public FacebookGraphException exceptionForTypeAndMessage(String type, String message) {
  455. if ("OAuthException".equals(type) || "OAuthAccessTokenException".equals(type))
  456. return new FacebookOAuthException(type, message);
  457. if ("QueryParseException".equals(type))
  458. return new FacebookQueryParseException(type, message);
  459. // Don't recognize this exception type? Just go with the standard
  460. // FacebookGraphException.
  461. return new FacebookGraphException(type, message);
  462. }
  463. };
  464. }
  465. /**
  466. * Generate the parameter string to be included in the Facebook API request.
  467. *
  468. * @param parameters Arbitrary number of extra parameters to include in the request.
  469. * @return The parameter string to include in the Facebook API request.
  470. * @throws FacebookJsonMappingException If an error occurs when building the parameter string.
  471. */
  472. protected String toParameterString(Parameter... parameters) {
  473. if (!isBlank(accessToken))
  474. parameters = parametersWithAdditionalParameter(Parameter.with(ACCESS_TOKEN_PARAM_NAME, accessToken), parameters);
  475. parameters = parametersWithAdditionalParameter(Parameter.with(FORMAT_PARAM_NAME, "json"), parameters);
  476. StringBuilder parameterStringBuilder = new StringBuilder();
  477. boolean first = true;
  478. for (Parameter parameter : parameters) {
  479. if (first)
  480. first = false;
  481. else
  482. parameterStringBuilder.append("&");
  483. parameterStringBuilder.append(urlEncode(parameter.name));
  484. parameterStringBuilder.append("=");
  485. parameterStringBuilder.append(urlEncodedValueForParameterName(parameter.name, parameter.value));
  486. }
  487. return parameterStringBuilder.toString();
  488. }
  489. /**
  490. * Returns the base endpoint URL for the Graph API.
  491. *
  492. * @return The base endpoint URL for the Graph API.
  493. */
  494. protected String getFacebookGraphEndpointUrl() {
  495. return FACEBOOK_GRAPH_ENDPOINT_URL;
  496. }
  497. /**
  498. * Returns the base endpoint URL for the Old REST API.
  499. *
  500. * @return The base endpoint URL for the Old REST API.
  501. */
  502. protected String getFacebookLegacyEndpointUrl() {
  503. return FACEBOOK_LEGACY_ENDPOINT_URL;
  504. }
  505. }