Skip to content

Commit

Permalink
Initial commit: Full refactor with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
peter-szrnka committed Nov 13, 2023
1 parent 1369cdb commit 4b980d5
Show file tree
Hide file tree
Showing 21 changed files with 1,312 additions and 476 deletions.
13 changes: 13 additions & 0 deletions code/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>${google-auth-library.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
Expand Down Expand Up @@ -128,6 +134,13 @@
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
package io.github.szrnkapeter.firebase.hosting;

import java.io.IOException;
import java.net.MalformedURLException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import com.google.common.annotations.VisibleForTesting;

import io.github.szrnkapeter.firebase.hosting.config.FirebaseHostingApiConfig;
import io.github.szrnkapeter.firebase.hosting.model.DeployItem;
import io.github.szrnkapeter.firebase.hosting.model.DeployRequest;
Expand All @@ -25,11 +12,26 @@
import io.github.szrnkapeter.firebase.hosting.model.Release;
import io.github.szrnkapeter.firebase.hosting.model.UploadFileRequest;
import io.github.szrnkapeter.firebase.hosting.model.Version;
import io.github.szrnkapeter.firebase.hosting.util.ConnectionUtils;
import io.github.szrnkapeter.firebase.hosting.util.Constants;
import io.github.szrnkapeter.firebase.hosting.service.FileService;
import io.github.szrnkapeter.firebase.hosting.service.ReleaseService;
import io.github.szrnkapeter.firebase.hosting.service.VersionService;
import io.github.szrnkapeter.firebase.hosting.service.impl.FileServiceImpl;
import io.github.szrnkapeter.firebase.hosting.service.impl.ReleaseServiceImpl;
import io.github.szrnkapeter.firebase.hosting.service.impl.VersionServiceImpl;
import io.github.szrnkapeter.firebase.hosting.util.FileUtils;
import io.github.szrnkapeter.firebase.hosting.util.GoogleCredentialUtils;

import java.io.IOException;
import java.net.MalformedURLException;
import java.security.NoSuchAlgorithmException;
import java.util.Set;
import java.util.stream.Collectors;

import static io.github.szrnkapeter.firebase.hosting.util.ConfigValidationUtils.preValidateConfig;
import static io.github.szrnkapeter.firebase.hosting.util.FileUtils.generateFileListAndHash;
import static io.github.szrnkapeter.firebase.hosting.util.VersionUtils.getVersionId;
import static io.github.szrnkapeter.firebase.hosting.util.VersionUtils.getVersionName;

/**
* Firebase REST API client for Java
*
Expand All @@ -38,12 +40,21 @@
*/
public class FirebaseHostingApiClient {

private static final String FILES = "/files";
private static final String VERSIONS = "/versions/";
private static final String SITES = "sites/";

private final FirebaseHostingApiConfig config;
private final String accessToken;

private final ReleaseService releaseService;
private final VersionService versionService;
private final FileService fileService;

public static FirebaseHostingApiClient newClient(FirebaseHostingApiConfig config) {
String accessToken = GoogleCredentialUtils.getAccessToken(config);

ReleaseService releaseService = new ReleaseServiceImpl(config, accessToken);
VersionService versionService = new VersionServiceImpl(config, accessToken);
FileService fileService = new FileServiceImpl(config, accessToken);

return new FirebaseHostingApiClient(config, releaseService, versionService, fileService);
}

/**
* Default constructor.
Expand All @@ -52,11 +63,14 @@ public class FirebaseHostingApiClient {
*
* @since 0.2
*/
public FirebaseHostingApiClient(FirebaseHostingApiConfig firebaseRestApiConfig) {
protected FirebaseHostingApiClient(FirebaseHostingApiConfig firebaseRestApiConfig, ReleaseService releaseService,
VersionService versionService, FileService fileService) {
preValidateConfig(firebaseRestApiConfig);

config = firebaseRestApiConfig;
accessToken = GoogleCredentialUtils.getAccessToken(config);
this.releaseService = releaseService;
this.versionService = versionService;
this.fileService = fileService;
}

/**
Expand Down Expand Up @@ -112,7 +126,7 @@ public DeployResponse createDeploy(DeployRequest request) throws IOException, No
PopulateFilesResponse populateFilesResponse = populateFiles(populateRequest, versionId);

// Upload them
uploadFiles(versionId, request, populateFilesResponse);
fileService.uploadFiles(versionId, request.getFiles(), populateFilesResponse.getUploadRequiredHashes());

// Finalize the new version
finalizeVersion(versionId);
Expand All @@ -121,7 +135,7 @@ public DeployResponse createDeploy(DeployRequest request) throws IOException, No
Release newRelease = createRelease(versionId);

// Post delete earlier deployments
deletePreviousVersion(request);
versionService.deletePreviousVersions(request, getReleases().getReleases());

// Create the release
return new DeployResponse(newRelease);
Expand All @@ -137,12 +151,8 @@ public DeployResponse createDeploy(DeployRequest request) throws IOException, No
*
* @since 0.2
*/
public Release createRelease(String version) throws IOException {
Release newRelease = ConnectionUtils.openSimpleHTTPPostConnection(config, Release.class, accessToken,
SITES + config.getSiteId() + "/releases?versionName=" + getVersionName(version), null, "createRelease");

responseCallback("createRelease", newRelease);
return newRelease;
public Release createRelease(String version) throws IOException {
return releaseService.createRelease(getVersionName(config, version));
}

/**
Expand All @@ -155,11 +165,7 @@ public Release createRelease(String version) throws IOException {
* @since 0.2
*/
public Version createVersion() throws IOException {
Version newVersion = ConnectionUtils.openSimpleHTTPPostConnection(config, Version.class, accessToken,
SITES + config.getSiteId() + "/versions", "{}", "createVersion");

responseCallback("createVersion", newVersion);
return newVersion;
return versionService.createVersion();
}

/**
Expand All @@ -172,7 +178,7 @@ public Version createVersion() throws IOException {
* @since 0.2
*/
public void deleteVersion(String version) throws IOException {
ConnectionUtils.openSimpleHTTPConnection("DELETE", config, null, accessToken, getVersionName(version), null, "deleteVersion");
versionService.deleteVersion(getVersionName(config, version));
}

/**
Expand All @@ -186,11 +192,7 @@ public void deleteVersion(String version) throws IOException {
* @since 0.2
*/
public Version finalizeVersion(String version) throws IOException {
Version newVersion = ConnectionUtils.openSimpleHTTPConnection("PATCH", config, Version.class, accessToken,
SITES + config.getSiteId() + VERSIONS + version + "?update_mask=status", "{ \"status\": \"FINALIZED\" }", "finalizeVersion");

responseCallback("finalizeVersion", newVersion);
return newVersion;
return versionService.finalizeVersion(version);
}

/**
Expand All @@ -204,11 +206,7 @@ public Version finalizeVersion(String version) throws IOException {
* @since 0.2
*/
public GetReleasesResponse getReleases() throws IOException {
GetReleasesResponse response = ConnectionUtils.openHTTPGetConnection(config, GetReleasesResponse.class, accessToken,
SITES + config.getSiteId() + "/releases");

responseCallback("getReleases", response);
return response;
return releaseService.getReleases();
}

/**
Expand All @@ -222,10 +220,7 @@ public GetReleasesResponse getReleases() throws IOException {
* @since 0.2
*/
public GetVersionFilesResponse getVersionFiles(String version) throws IOException {
GetVersionFilesResponse response = ConnectionUtils.openHTTPGetConnection(config, GetVersionFilesResponse.class, accessToken, getVersionName(version) + FILES);

responseCallback("getVersionFiles", response);
return response;
return fileService.getVersionFiles(getVersionName(config, version));
}

/**
Expand All @@ -240,13 +235,8 @@ public GetVersionFilesResponse getVersionFiles(String version) throws IOExceptio
* @since 0.2
*/
public PopulateFilesResponse populateFiles(PopulateFilesRequest request, String version) throws IOException {
String data = config.getSerializer().toJson(PopulateFilesRequest.class, request);
PopulateFilesResponse response = ConnectionUtils.openSimpleHTTPPostConnection(config, PopulateFilesResponse.class, accessToken, SITES + config.getSiteId() + VERSIONS + version + ":populateFiles", data, "populateFiles");

responseCallback("populateFiles", response);
return response;
return fileService.populateFiles(request, version);
}


/**
* Uploads a file.
Expand All @@ -258,111 +248,6 @@ public PopulateFilesResponse populateFiles(PopulateFilesRequest request, String
* @since 0.2
*/
public void uploadFile(UploadFileRequest request) throws NoSuchAlgorithmException, IOException {
String calculatedHash = FileUtils.getSHA256Checksum(request.getFileContent());
String url = Constants.UPLOAD_FIREBASE_API_URL + "upload/" + SITES + config.getSiteId() + VERSIONS + request.getVersion() + FILES + "/" + calculatedHash;

if(request.getUploadUrl() != null) {
url = request.getUploadUrl() + "/" + calculatedHash;
}

ConnectionUtils.uploadFile(config, accessToken, request.getFileName(), url, request.getFileContent());
}

@VisibleForTesting
String getVersionId(String version) {
if(version == null || version.isEmpty()) {
return null;
}

return version.substring(version.lastIndexOf("/") + 1);
}

private void responseCallback(String function, Object response) {
if(config.getServiceResponseListener() == null) {
return;
}

config.getServiceResponseListener().getResponse(function, response);
}

private void deletePreviousVersion(DeployRequest request) throws IOException {
if(!request.isDeletePreviousVersions()) {
return;
}

GetReleasesResponse response = getReleases();
AtomicInteger i = new AtomicInteger(0);

for(Release release : response.getReleases()) {
if(i.get() > 0 && Constants.FINALIZED.equals(release.getVersion().getStatus())) {
deleteVersion(release.getVersion().getName());
}

i.incrementAndGet();
}
}

private String getVersionName(String version) {
if(version == null || version.isEmpty()) {
throw new IllegalArgumentException("Version field is mandatory!");
}

String versionName = SITES + config.getSiteId() + VERSIONS + version;

Pattern p = Pattern.compile(SITES + config.getSiteId() + VERSIONS + ".*");
Matcher m = p.matcher(version);

if (m.matches()) {
versionName = version;
}

return versionName;
}

private void uploadFiles(String versionId, DeployRequest request, PopulateFilesResponse populateFilesResponse) throws IOException, NoSuchAlgorithmException {
for(DeployItem item : request.getFiles()) {
byte[] fileContent = FileUtils.compressAndReadFile(item.getContent());
String checkSum = FileUtils.getSHA256Checksum(fileContent);

if(populateFilesResponse.getUploadRequiredHashes() != null && !populateFilesResponse.getUploadRequiredHashes().contains(checkSum)) {
continue;
}

UploadFileRequest uploadFilesRequest = new UploadFileRequest();
uploadFilesRequest.setVersion(versionId);
uploadFilesRequest.setFileContent(fileContent);
uploadFilesRequest.setFileName(item.getName());
uploadFile(uploadFilesRequest);
}
}

private Map<String, String> generateFileListAndHash(Set<DeployItem> files) throws IOException, NoSuchAlgorithmException {
Map<String, String> result = new HashMap<>();

for(DeployItem file : files) {
byte[] gzippedContent = FileUtils.compressAndReadFile(file.getContent());
String checkSum = FileUtils.getSHA256Checksum(gzippedContent);
result.put("/" + file.getName(), checkSum);
}

return result;
}

private void preValidateConfig(FirebaseHostingApiConfig firebaseRestApiConfig) {
if (firebaseRestApiConfig == null) {
throw new IllegalArgumentException("FirebaseRestApiConfig field is mandatory!");
}

if (firebaseRestApiConfig.getSiteId() == null || firebaseRestApiConfig.getSiteId().isEmpty()) {
throw new IllegalArgumentException("Site name is mandatory!");
}

if (firebaseRestApiConfig.getServiceAccountFileStream() == null) {
throw new IllegalArgumentException("Service account file stream is missing from the configuration!");
}

if (firebaseRestApiConfig.getSerializer() == null) {
throw new IllegalArgumentException("Serializer is missing from the configuration!");
}
fileService.uploadFile(request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.github.szrnkapeter.firebase.hosting.service;

import io.github.szrnkapeter.firebase.hosting.config.FirebaseHostingApiConfig;

/**
* @author Peter Szrnka
* @since 0.8
*/
public abstract class AbstractUtilityService {

protected final FirebaseHostingApiConfig config;
protected final String accessToken;

protected AbstractUtilityService(FirebaseHostingApiConfig config, String accessToken) {
this.config = config;
this.accessToken = accessToken;
}

protected void responseCallback(String function, Object response) {
if(config.getServiceResponseListener() == null) {
return;
}

config.getServiceResponseListener().getResponse(function, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.github.szrnkapeter.firebase.hosting.service;

import io.github.szrnkapeter.firebase.hosting.model.DeployItem;
import io.github.szrnkapeter.firebase.hosting.model.GetVersionFilesResponse;
import io.github.szrnkapeter.firebase.hosting.model.PopulateFilesRequest;
import io.github.szrnkapeter.firebase.hosting.model.PopulateFilesResponse;
import io.github.szrnkapeter.firebase.hosting.model.UploadFileRequest;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Set;

/**
* @author Peter Szrnka
* @since 0.8
*/
public interface FileService {

GetVersionFilesResponse getVersionFiles(String versionName) throws IOException;

PopulateFilesResponse populateFiles(PopulateFilesRequest request, String version) throws IOException;

void uploadFile(UploadFileRequest request) throws NoSuchAlgorithmException, IOException;

void uploadFiles(String versionId, Set<DeployItem> files, List<String> requiredHashes) throws IOException, NoSuchAlgorithmException;
}
Loading

0 comments on commit 4b980d5

Please sign in to comment.