ThreescaleCmsClientImpl.java
package com.fwmotion.threescale.cms;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fwmotion.threescale.cms.exception.*;
import com.fwmotion.threescale.cms.mappers.CmsFileMapper;
import com.fwmotion.threescale.cms.mappers.CmsSectionMapper;
import com.fwmotion.threescale.cms.mappers.CmsTemplateMapper;
import com.fwmotion.threescale.cms.model.*;
import com.fwmotion.threescale.cms.support.PagedFilesSpliterator;
import com.fwmotion.threescale.cms.support.PagedSectionsSpliterator;
import com.fwmotion.threescale.cms.support.PagedTemplatesSpliterator;
import com.redhat.threescale.rest.cms.ApiClient;
import com.redhat.threescale.rest.cms.ApiException;
import com.redhat.threescale.rest.cms.api.FilesApi;
import com.redhat.threescale.rest.cms.api.SectionsApi;
import com.redhat.threescale.rest.cms.api.TemplatesApi;
import com.redhat.threescale.rest.cms.model.Error;
import com.redhat.threescale.rest.cms.model.*;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpHeaders;
import org.mapstruct.factory.Mappers;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Optional;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class ThreescaleCmsClientImpl implements ThreescaleCmsClient {
private static final CmsFileMapper FILE_MAPPER = Mappers.getMapper(CmsFileMapper.class);
private static final CmsSectionMapper SECTION_MAPPER = Mappers.getMapper(CmsSectionMapper.class);
private static final CmsTemplateMapper TEMPLATE_MAPPER = Mappers.getMapper(CmsTemplateMapper.class);
private final FilesApi filesApi;
private final SectionsApi sectionsApi;
private final TemplatesApi templatesApi;
private final ObjectMapper objectMapper;
public ThreescaleCmsClientImpl(@Nonnull FilesApi filesApi,
@Nonnull SectionsApi sectionsApi,
@Nonnull TemplatesApi templatesApi,
@Nonnull ObjectMapper objectMapper) {
this.filesApi = filesApi;
this.sectionsApi = sectionsApi;
this.templatesApi = templatesApi;
this.objectMapper = objectMapper;
}
public ThreescaleCmsClientImpl(@Nonnull ApiClient apiClient) {
this(new FilesApi(apiClient),
new SectionsApi(apiClient),
new TemplatesApi(apiClient),
apiClient.getObjectMapper());
}
private <T> T handleApiErrors(
@Nonnull ApiBlock<T> apiBlock,
@Nullable ApiExceptionTransformer<?> exceptionTransformer
) throws ThreescaleCmsApiException {
try {
return apiBlock.callApi();
} catch (ApiException e) {
throw handleApiException(exceptionTransformer, e);
}
}
private <T> T handleApiErrors(@Nonnull ApiBlock<T> apiBlock) throws ThreescaleCmsException {
return handleApiErrors(apiBlock, null);
}
private void handleApiErrors(
@Nonnull VoidApiBlock apiBlock,
@Nullable ApiExceptionTransformer<?> exceptionTransformer
) throws ThreescaleCmsException {
try {
apiBlock.callApi();
} catch (ApiException e) {
throw handleApiException(exceptionTransformer, e);
}
}
private void handleApiErrors(@Nonnull VoidApiBlock apiBlock) throws ThreescaleCmsException {
handleApiErrors(apiBlock, null);
}
@Nonnull
private ThreescaleCmsApiException handleApiException(ApiExceptionTransformer<?> exceptionTransformer, ApiException e) {
int httpStatus = e.getCode();
// Try to deserialize a REST-modeled Error object type
Error responseError;
try {
responseError = objectMapper.readValue(e.getResponseBody(), Error.class);
} catch (JsonProcessingException jsonProcessingException) {
// If the response body is not parseable into an Error, throw
// the response body as-is.
return new ThreescaleCmsApiException(httpStatus, "Unknown ApiException", e);
}
ThreescaleCmsApiException apiException = new ThreescaleCmsApiException(
httpStatus,
responseError,
e);
if (exceptionTransformer == null) {
return apiException;
}
return exceptionTransformer.transformException(apiException);
}
@Nonnull
@Override
public Stream<CmsSection> streamSections() {
return StreamSupport.stream(new PagedSectionsSpliterator(sectionsApi, objectMapper), true);
}
@Nonnull
@Override
public Stream<CmsFile> streamFiles() {
return StreamSupport.stream(new PagedFilesSpliterator(filesApi, objectMapper), true);
}
@Nonnull
@Override
public Optional<InputStream> getFileContent(long fileId) {
return handleApiErrors(() -> {
CloseableHttpClient httpClient = filesApi.getApiClient().getHttpClient();
ModelFile file = filesApi.getFile(fileId);
ProviderAccount account = filesApi.readProviderSettings().getAccount();
HttpGet request = new HttpGet(account.getBaseUrl() + file.getPath());
request.setHeader(HttpHeaders.ACCEPT, "*/*");
if (StringUtils.isNotEmpty(account.getSiteAccessCode())) {
request.addHeader("Cookie", "access_code=" + account.getSiteAccessCode());
}
try {
return httpClient.execute(request, response -> {
if (response == null) {
return Optional.empty();
}
// TODO: Validate response headers, status code, etc
HttpEntity entity = response.getEntity();
if (entity == null) {
return Optional.empty();
}
return Optional.of(
new ByteArrayInputStream(entity.getContent().readAllBytes())
);
});
} catch (IOException e) {
throw new ThreescaleCmsNonApiException("IOException while retrieving file content", e);
}
});
}
@Nonnull
@Override
public Stream<CmsTemplate> streamTemplates(boolean includeContent) {
return StreamSupport.stream(new PagedTemplatesSpliterator(templatesApi, objectMapper, includeContent), true);
}
@Nonnull
@Override
public Optional<InputStream> getTemplateDraft(long templateId) {
return handleApiErrors(() -> {
Template template = templatesApi.getTemplate(templateId);
Optional<InputStream> result = Optional.ofNullable(template.getDraft())
.map(StringUtils::trimToNull)
.map(input -> IOUtils.toInputStream(input, Charset.defaultCharset()));
// When there's no draft content, the "draft" should be the same as
// the "published" content
if (result.isEmpty()) {
result = Optional.ofNullable(template.getPublished())
.map(StringUtils::trimToNull)
.map(input -> IOUtils.toInputStream(input, Charset.defaultCharset()));
}
return result;
});
}
@Nonnull
@Override
public Optional<InputStream> getTemplatePublished(long templateId) {
return handleApiErrors(() -> {
Template template = templatesApi.getTemplate(templateId);
return Optional.ofNullable(template.getPublished())
.map(input -> IOUtils.toInputStream(input, Charset.defaultCharset()));
});
}
@Override
public void save(@Nonnull CmsSection section) {
handleApiErrors(() -> {
Section restSection = SECTION_MAPPER.toRest(section);
if (section.getId() == null) {
if (StringUtils.isBlank(restSection.getTitle())
&& StringUtils.isNotBlank(restSection.getSystemName())) {
restSection.setTitle(restSection.getSystemName());
}
Section response = sectionsApi.createSection(
restSection.getPublic(),
restSection.getTitle(),
restSection.getParentId(),
restSection.getPartialPath(),
restSection.getSystemName());
section.setId(response.getId());
} else {
sectionsApi.updateSection(restSection.getId(),
restSection.getPublic(),
restSection.getTitle(),
restSection.getParentId());
}
});
}
@Override
public void save(@Nonnull CmsFile file, @Nullable File fileContent) {
handleApiErrors(() -> {
ModelFile restFile = FILE_MAPPER.toRest(file);
if (file.getId() == null) {
ModelFile response = filesApi.createFile(
restFile.getSectionId(),
restFile.getPath(),
fileContent,
restFile.getDownloadable(),
restFile.getContentType());
file.setId(response.getId());
} else {
filesApi.updateFile(file.getId(),
restFile.getSectionId(),
restFile.getPath(),
restFile.getDownloadable(),
fileContent,
restFile.getContentType());
}
});
}
@Override
public void save(@Nonnull CmsTemplate template, @Nullable File templateDraft) {
/* When upgraded to JDK21:
switch (template) {
case CmsBuiltinPage cmsBuiltinPage -> saveBuiltinPage(cmsBuiltinPage, templateDraft);
case CmsBuiltinPartial cmsBuiltinPartial -> saveBuiltinPartial(cmsBuiltinPartial, templateDraft);
case CmsLayout cmsLayout -> saveLayout(cmsLayout, templateDraft);
case CmsPage cmsPage -> savePage(cmsPage, templateDraft);
case CmsPartial cmsPartial -> savePartial(cmsPartial, templateDraft);
default -> throw new UnsupportedOperationException("Unknown template type: " + template.getClass().getName());
}
*/
if (template instanceof CmsBuiltinPage cmsBuiltinPage) {
saveBuiltinPage(cmsBuiltinPage, templateDraft);
} else if (template instanceof CmsBuiltinPartial cmsBuiltinPartial) {
saveBuiltinPartial(cmsBuiltinPartial, templateDraft);
} else if (template instanceof CmsLayout cmsLayout) {
saveLayout(cmsLayout, templateDraft);
} else if (template instanceof CmsPage cmsPage) {
savePage(cmsPage, templateDraft);
} else if (template instanceof CmsPartial cmsPartial) {
savePartial(cmsPartial, templateDraft);
} else {
throw new UnsupportedOperationException("Unknown template type: " + template.getClass().getName());
}
}
private void saveBuiltinPage(@Nonnull CmsBuiltinPage page, @Nullable File templateDraft) {
if (page.getId() == null) {
throw new ThreescaleCmsCannotCreateBuiltinException("Built-in pages can't be created.");
}
saveUpdatedTemplate(page.getId(),
TEMPLATE_MAPPER.toRestBuiltinPage(page),
templateDraft);
}
private void saveBuiltinPartial(@Nonnull CmsBuiltinPartial partial, @Nullable File templateDraft) {
if (partial.getId() == null) {
throw new ThreescaleCmsCannotCreateBuiltinException("Built-in partials cannot be created.");
}
saveUpdatedTemplate(partial.getId(),
TEMPLATE_MAPPER.toRestBuiltinPartial(partial),
templateDraft);
}
private void saveLayout(@Nonnull CmsLayout layout, @Nullable File templateDraft) {
if (layout.getId() == null) {
Template response = saveNewTemplate(
TEMPLATE_MAPPER.toRestLayoutCreation(layout),
templateDraft);
layout.setId(response.getId());
} else {
saveUpdatedTemplate(layout.getId(),
TEMPLATE_MAPPER.toRestLayoutUpdate(layout),
templateDraft);
}
}
private void savePage(@Nonnull CmsPage page, @Nullable File templateDraft) {
if (page.getId() == null) {
Template response = saveNewTemplate(
TEMPLATE_MAPPER.toRestPageCreation(page), templateDraft);
page.setId(response.getId());
} else {
saveUpdatedTemplate(page.getId(),
TEMPLATE_MAPPER.toRestPageUpdate(page),
templateDraft);
}
}
private void savePartial(@Nonnull CmsPartial partial, @Nullable File templateDraft) {
if (partial.getId() == null) {
Template response = saveNewTemplate(
TEMPLATE_MAPPER.toRestPartialCreation(partial),
templateDraft);
partial.setId(response.getId());
} else {
saveUpdatedTemplate(partial.getId(),
TEMPLATE_MAPPER.toRestPartialUpdate(partial),
templateDraft);
}
}
private Template saveNewTemplate(@Nonnull TemplateCreationRequest template, @Nullable File templateDraft) {
if (templateDraft == null) {
throw new IllegalArgumentException("New template must have draft content");
}
String draft;
try {
draft = FileUtils.readFileToString(templateDraft, Charset.defaultCharset());
} catch (IOException e) {
throw new ThreescaleCmsNonApiException("Exception while reading file content for template draft", e);
}
return handleApiErrors(() -> templatesApi.createTemplate(template.getType(),
template.getSystemName(),
template.getTitle(),
template.getPath(),
draft,
template.getSectionId(),
template.getLayoutName(),
template.getLayoutId(),
template.getLiquidEnabled(),
template.getHandler(),
template.getContentType()));
}
@SuppressWarnings("UnusedReturnValue")
private Template saveUpdatedTemplate(long id, @Nonnull TemplateUpdatableFields template, @Nullable File templateDraft) {
String draft;
if (templateDraft == null) {
draft = null;
} else {
try {
draft = FileUtils.readFileToString(templateDraft, Charset.defaultCharset());
} catch (IOException e) {
throw new ThreescaleCmsNonApiException("Exception while reading file content for template draft", e);
}
}
return handleApiErrors(() -> templatesApi.updateTemplate(id,
template.getSystemName(),
template.getTitle(),
template.getPath(),
draft,
template.getSectionId(),
template.getLayoutName(),
template.getLayoutId(),
template.getLiquidEnabled(),
template.getHandler(),
template.getContentType()));
}
@Override
public void publish(long templateId) throws ThreescaleCmsApiException {
handleApiErrors(() -> templatesApi.publishTemplate(templateId));
}
@Override
public void delete(@Nonnull ThreescaleObjectType type, long id) throws ThreescaleCmsApiException {
handleApiErrors(
() -> {
switch (type) {
case SECTION:
sectionsApi.deleteSection(id);
break;
case FILE:
filesApi.deleteFile(id);
break;
case TEMPLATE:
templatesApi.deleteTemplate(id);
break;
default:
throw new UnsupportedOperationException("Unknown type: " + type);
}
},
apiException -> {
if (apiException.getHttpStatus() == ThreescaleCmsCannotDeleteBuiltinException.ERROR_HTTP_CODE
&& apiException.getApiError()
.filter(apiError -> ThreescaleCmsCannotDeleteBuiltinException.ERROR_MESSAGE.equals(apiError.getError()))
.isPresent()
) {
return new ThreescaleCmsCannotDeleteBuiltinException(
apiException.getApiError().get()
);
}
return apiException;
}
);
}
@FunctionalInterface
private interface VoidApiBlock {
void callApi() throws ApiException;
}
@FunctionalInterface
private interface ApiBlock<T> {
T callApi() throws ApiException;
}
@FunctionalInterface
private interface ApiExceptionTransformer<T extends ThreescaleCmsApiException> {
T transformException(ThreescaleCmsApiException e);
}
}