|
| 1 | +/* |
| 2 | + * Copyright 2022 Google LLC |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package com.example.analytics; |
| 18 | + |
| 19 | +import com.google.analytics.data.v1beta.BetaAnalyticsDataClient; |
| 20 | +import com.google.analytics.data.v1beta.BetaAnalyticsDataSettings; |
| 21 | +import com.google.analytics.data.v1beta.DateRange; |
| 22 | +import com.google.analytics.data.v1beta.Dimension; |
| 23 | +import com.google.analytics.data.v1beta.Metric; |
| 24 | +import com.google.analytics.data.v1beta.Row; |
| 25 | +import com.google.analytics.data.v1beta.RunReportRequest; |
| 26 | +import com.google.analytics.data.v1beta.RunReportResponse; |
| 27 | +import com.google.api.client.http.GenericUrl; |
| 28 | +import com.google.api.client.http.HttpStatusCodes; |
| 29 | +import com.google.api.client.util.Key; |
| 30 | +import com.google.api.gax.core.FixedCredentialsProvider; |
| 31 | +import com.google.auth.oauth2.ClientId; |
| 32 | +import com.google.auth.oauth2.UserAuthorizer; |
| 33 | +import com.google.auth.oauth2.UserCredentials; |
| 34 | +import com.google.common.base.MoreObjects; |
| 35 | +import com.google.common.base.Strings; |
| 36 | +import com.google.common.collect.ImmutableList; |
| 37 | +import java.io.BufferedReader; |
| 38 | +import java.io.FileInputStream; |
| 39 | +import java.io.IOException; |
| 40 | +import java.io.InputStreamReader; |
| 41 | +import java.io.OutputStreamWriter; |
| 42 | +import java.io.Writer; |
| 43 | +import java.math.BigInteger; |
| 44 | +import java.net.ServerSocket; |
| 45 | +import java.net.Socket; |
| 46 | +import java.net.URI; |
| 47 | +import java.nio.charset.StandardCharsets; |
| 48 | +import java.security.SecureRandom; |
| 49 | +import java.util.regex.Matcher; |
| 50 | +import java.util.regex.Pattern; |
| 51 | + |
| 52 | +/** |
| 53 | + * Google Analytics Data API sample quickstart application. |
| 54 | + * |
| 55 | + * <p>This application demonstrates the usage of the Analytics Data API using service account |
| 56 | + * credentials. |
| 57 | + * |
| 58 | + * <p>Before you start the application, please review the comments starting with "TODO(developer)" |
| 59 | + * and update the code to use correct values. |
| 60 | + * |
| 61 | + * <p>To run this sample using Maven: |
| 62 | + * |
| 63 | + * <pre> |
| 64 | + * cd google-analytics-data |
| 65 | + * mvn compile |
| 66 | + * mvn exec:java -Dexec.mainClass="com.example.analytics.QuickstartOAuth2Sample" |
| 67 | + * </pre> |
| 68 | + */ |
| 69 | +// [START analyticsdata_quickstart_oauth2] |
| 70 | +public class QuickstartOAuth2Sample { |
| 71 | + // Scopes for the generated OAuth2 credentials. The list here only contains the Google Ads API |
| 72 | + // scope, but you can add multiple scopes if you want to use the credentials for other Google |
| 73 | + // APIs. |
| 74 | + private static final ImmutableList<String> SCOPES = |
| 75 | + ImmutableList.<String>builder() |
| 76 | + .add("https://www.googleapis.com/auth/analytics.readonly") |
| 77 | + .build(); |
| 78 | + private static final String OAUTH2_CALLBACK_BASE_URI = "http://127.0.0.1"; |
| 79 | + |
| 80 | + public static void main(String... args) throws Exception { |
| 81 | + /** |
| 82 | + * TODO(developer): Replace this variable with your Google Analytics 4 property ID before |
| 83 | + * running the sample. |
| 84 | + */ |
| 85 | + String propertyId = "YOUR-GA4-PROPERTY-ID"; |
| 86 | + sampleGetCredentialsAndRunReport(propertyId); |
| 87 | + } |
| 88 | + |
| 89 | + // This is an example snippet that calls the Google Analytics Data API and runs |
| 90 | + // a simple report |
| 91 | + // on the provided GA4 property id. |
| 92 | + static void sampleGetCredentialsAndRunReport(String propertyId) throws Exception { |
| 93 | + // Extracts the OAuth client information from the provided file. |
| 94 | + ClientId parsedClient = ClientId.fromStream(new FileInputStream("./oauth2.keys.json")); |
| 95 | + // Creates an anti-forgery state token as described here: |
| 96 | + // https://developers.google.com/identity/protocols/OpenIDConnect#createxsrftoken |
| 97 | + String state = new BigInteger(130, new SecureRandom()).toString(32); |
| 98 | + |
| 99 | + // Creates an HTTP server that will listen for the OAuth2 callback request. |
| 100 | + URI baseUri; |
| 101 | + UserAuthorizer userAuthorizer; |
| 102 | + AuthorizationResponse authorizationResponse = null; |
| 103 | + try (SimpleCallbackServer simpleCallbackServer = new SimpleCallbackServer()) { |
| 104 | + userAuthorizer = |
| 105 | + UserAuthorizer.newBuilder() |
| 106 | + .setClientId(parsedClient) |
| 107 | + .setScopes(SCOPES) |
| 108 | + // Provides an empty callback URI so that no additional suffix is added to the |
| 109 | + // redirect. By default, UserAuthorizer will use "/oauth2callback" if this is |
| 110 | + // either not set or set to null. |
| 111 | + .setCallbackUri(URI.create("")) |
| 112 | + .build(); |
| 113 | + baseUri = URI.create(OAUTH2_CALLBACK_BASE_URI + ":" + simpleCallbackServer.getLocalPort()); |
| 114 | + System.out.printf( |
| 115 | + "Paste this url in your browser:%n%s%n", |
| 116 | + userAuthorizer.getAuthorizationUrl(null, state, baseUri)); |
| 117 | + |
| 118 | + // Waits for the authorization code. |
| 119 | + simpleCallbackServer.accept(); |
| 120 | + authorizationResponse = simpleCallbackServer.authorizationResponse; |
| 121 | + } |
| 122 | + |
| 123 | + if (authorizationResponse == null || authorizationResponse.code == null) { |
| 124 | + throw new NullPointerException( |
| 125 | + "OAuth2 callback did not contain an authorization code: " + authorizationResponse); |
| 126 | + } |
| 127 | + |
| 128 | + // Confirms that the state in the response matches the state token used to |
| 129 | + // generate the |
| 130 | + // authorization URL. |
| 131 | + if (!state.equals(authorizationResponse.state)) { |
| 132 | + throw new IllegalStateException("State does not match expected state"); |
| 133 | + } |
| 134 | + |
| 135 | + // Exchanges the authorization code for credentials and print the refresh token. |
| 136 | + UserCredentials userCredentials = |
| 137 | + userAuthorizer.getCredentialsFromCode(authorizationResponse.code, baseUri); |
| 138 | + System.out.printf("Successfully obtained user credentials for scope(s): %s%n", SCOPES); |
| 139 | + |
| 140 | + // Constructs a BetaAnalyticsDataClient using the UserCredentials obtained. |
| 141 | + try (BetaAnalyticsDataClient analyticsData = |
| 142 | + BetaAnalyticsDataClient.create( |
| 143 | + BetaAnalyticsDataSettings.newBuilder() |
| 144 | + .setCredentialsProvider(FixedCredentialsProvider.create(userCredentials)) |
| 145 | + .build())) { |
| 146 | + RunReportRequest request = |
| 147 | + RunReportRequest.newBuilder() |
| 148 | + .setProperty("properties/" + propertyId) |
| 149 | + .addDimensions(Dimension.newBuilder().setName("city")) |
| 150 | + .addMetrics(Metric.newBuilder().setName("activeUsers")) |
| 151 | + .addDateRanges(DateRange.newBuilder().setStartDate("2020-03-31").setEndDate("today")) |
| 152 | + .build(); |
| 153 | + |
| 154 | + // Make the request. |
| 155 | + RunReportResponse response = analyticsData.runReport(request); |
| 156 | + |
| 157 | + System.out.println("Report result:"); |
| 158 | + // Iterate through every row of the API response. |
| 159 | + for (Row row : response.getRowsList()) { |
| 160 | + System.out.printf( |
| 161 | + "%s, %s%n", row.getDimensionValues(0).getValue(), row.getMetricValues(0).getValue()); |
| 162 | + } |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + /** Basic server that listens for the OAuth2 callback. */ |
| 167 | + private static class SimpleCallbackServer extends ServerSocket { |
| 168 | + |
| 169 | + private AuthorizationResponse authorizationResponse; |
| 170 | + |
| 171 | + SimpleCallbackServer() throws IOException { |
| 172 | + // Passes a port # of zero so that a port will be automatically allocated. |
| 173 | + super(0); |
| 174 | + } |
| 175 | + |
| 176 | + /** |
| 177 | + * Blocks until a connection is made to this server. After this method completes, the |
| 178 | + * authorizationResponse of this server will be set, provided the request line is in the |
| 179 | + * expected format. |
| 180 | + */ |
| 181 | + @Override |
| 182 | + public Socket accept() throws IOException { |
| 183 | + Socket socket = super.accept(); |
| 184 | + |
| 185 | + try (BufferedReader in = |
| 186 | + new BufferedReader( |
| 187 | + new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) { |
| 188 | + String callbackRequest = in.readLine(); |
| 189 | + // Uses a regular expression to extract the request line from the first line of |
| 190 | + // the |
| 191 | + // callback request, e.g.: |
| 192 | + // GET /?code=AUTH_CODE&state=XYZ&scope=https://www.googleapis.com/auth/adwords |
| 193 | + // HTTP/1.1 |
| 194 | + Pattern pattern = Pattern.compile("GET +([^ ]+)"); |
| 195 | + Matcher matcher = pattern.matcher(Strings.nullToEmpty(callbackRequest)); |
| 196 | + if (matcher.find()) { |
| 197 | + String relativeUrl = matcher.group(1); |
| 198 | + authorizationResponse = new AuthorizationResponse(OAUTH2_CALLBACK_BASE_URI + relativeUrl); |
| 199 | + } |
| 200 | + try (Writer outputWriter = new OutputStreamWriter(socket.getOutputStream())) { |
| 201 | + outputWriter.append("HTTP/1.1 "); |
| 202 | + outputWriter.append(Integer.toString(HttpStatusCodes.STATUS_CODE_OK)); |
| 203 | + outputWriter.append(" OK\n"); |
| 204 | + outputWriter.append("Content-Type: text/html\n\n"); |
| 205 | + |
| 206 | + outputWriter.append("<b>"); |
| 207 | + if (authorizationResponse.code != null) { |
| 208 | + outputWriter.append("Authorization code was successfully retrieved."); |
| 209 | + } else { |
| 210 | + outputWriter.append("Failed to retrieve authorization code."); |
| 211 | + } |
| 212 | + outputWriter.append("</b>"); |
| 213 | + outputWriter.append("<p>Please check the console output from <code>"); |
| 214 | + outputWriter.append(QuickstartOAuth2Sample.class.getSimpleName()); |
| 215 | + outputWriter.append("</code> for further instructions."); |
| 216 | + } |
| 217 | + } |
| 218 | + return socket; |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + /** Response object with attributes corresponding to OAuth2 callback parameters. */ |
| 223 | + static class AuthorizationResponse extends GenericUrl { |
| 224 | + |
| 225 | + /** The authorization code to exchange for an access token and (optionally) a refresh token. */ |
| 226 | + @Key String code; |
| 227 | + |
| 228 | + /** Error from the request or from the processing of the request. */ |
| 229 | + @Key String error; |
| 230 | + |
| 231 | + /** State parameter from the callback request. */ |
| 232 | + @Key String state; |
| 233 | + |
| 234 | + /** |
| 235 | + * Constructs a new instance based on an absolute URL. All fields annotated with the {@link Key} |
| 236 | + * annotation will be set if they are present in the URL. |
| 237 | + * |
| 238 | + * @param encodedUrl absolute URL with query parameters. |
| 239 | + */ |
| 240 | + public AuthorizationResponse(String encodedUrl) { |
| 241 | + super(encodedUrl); |
| 242 | + } |
| 243 | + |
| 244 | + @Override |
| 245 | + public String toString() { |
| 246 | + return MoreObjects.toStringHelper(getClass()) |
| 247 | + .add("code", code) |
| 248 | + .add("error", error) |
| 249 | + .add("state", state) |
| 250 | + .toString(); |
| 251 | + } |
| 252 | + } |
| 253 | +} |
| 254 | +// [END analyticsdata_quickstart_oauth2] |
0 commit comments