gocontentful/test/testapi/gocontentfulvolib.go

2933 lines
87 KiB
Go

// Code generated by https://github.com/foomo/gocontentful - DO NOT EDIT.
package testapi
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"regexp"
"strings"
"sync"
"time"
"unicode"
"github.com/foomo/contentful"
"golang.org/x/net/html"
"golang.org/x/sync/errgroup"
)
type cacheEntryMaps struct {
brand map[string]*CfBrand
category map[string]*CfCategory
product map[string]*CfProduct
}
type ClientMode string
type ContentfulCache struct {
assets assetCacheMap
contentTypes []string
entryMaps cacheEntryMaps
genericEntries map[string]*GenericEntry
idContentTypeMap map[string]string
parentMap map[string][]EntryReference
tags tagsCacheMap
}
type ContentfulCacheMutex struct {
fullCacheGcLock sync.RWMutex
sharedDataGcLock sync.RWMutex
assetsGcLock sync.RWMutex
tagGcLock sync.RWMutex
idContentTypeMapGcLock sync.RWMutex
parentMapGcLock sync.RWMutex
genericEntriesGcLock sync.RWMutex
brandGcLock sync.RWMutex
categoryGcLock sync.RWMutex
productGcLock sync.RWMutex
}
type assetCacheMap map[string]*contentful.Asset
type tagsCacheMap map[string]string
type ContentfulClient struct {
Cache *ContentfulCache
cacheInit bool
cacheMutex *ContentfulCacheMutex
cacheQueue chan struct{}
cacheDone chan struct{}
cacheUpdateTimeout int64
cacheWorkerOnce sync.Once
clientMode ClientMode
Client *contentful.Contentful
locales []Locale
logFn func(
fields map[string]interface{},
level int,
args ...interface{},
)
logLevel int
optimisticPageSize uint16
SpaceID string
offline bool
offlineTemp offlineTemp
sync bool
syncToken string
textJanitor bool
}
type offlineTemp struct {
Entries []contentful.Entry `json:"entries"`
Assets []contentful.Asset `json:"assets"`
Tags []contentful.Tag `json:"tags"`
}
type ContentTypeResult struct {
EntryID string
ContentType string
References map[string][]EntryReference
}
type ContentTypeInfo struct {
ContentType string
Title string
Description string
}
type ContentTypeInfoMap map[string]ContentTypeInfo
type EntryReference struct {
ContentType string
ID string
VO interface{}
CC *ContentfulClient
FromField string
}
type entryOrRef interface {
GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error)
}
type BrokenReference struct {
ParentID string `json:"parentId"`
ParentType string `json:"parentType"`
FromField string `json:"fromField"`
ChildID string `json:"childId"`
}
type ImageResolverFunc func(assetID string, locale Locale) (attrs map[string]string, customHTML string, resolveError error)
type EntryLinkResolverFunc func(entryID string, locale Locale) (resolvedAttrs map[string]string, resolveError error)
type LinkResolverFunc func(url string) (resolvedAttrs map[string]string, resolveError error)
type EmbeddedEntryResolverFunc func(entryID string, locale Locale) (htmlSnippet string, resolveError error)
type Locale string
const (
ClientModeCDA ClientMode = "CDA"
ClientModeCPA ClientMode = "CPA"
ClientModeCMA ClientMode = "CMA"
)
const (
LogDebug = 0
LogInfo = 1
LogWarn = 2
LogError = 3
)
const SpaceLocaleGerman Locale = "de"
const SpaceLocaleFrench Locale = "fr"
var SpaceLocales = []Locale{
"de",
"fr",
}
const DefaultLocale Locale = SpaceLocaleGerman
var localeFallback = map[Locale]Locale{SpaceLocaleGerman: "", SpaceLocaleFrench: ""}
const (
assetPageSize = 1000
assetWorkerType = "_asset"
tagWorkerType = "_tag"
)
const cacheUpdateConcurrency = 4
const (
sysTypeEntry = "Entry"
sysTypeAsset = "Asset"
sysTypeDeletedEntry = "DeletedEntry"
sysTypeDeletedAsset = "DeletedAsset"
)
var (
ErrLocaleUnsupported = errors.New("locale not supported by this space")
ErrNotSet = errors.New("field value not set")
ErrNotSetNoFallback = errors.New("field value not set and no fallback locale available")
ErrRefNotIncludes = errors.New("referenced entry not found in includes")
ErrNoTypeOfRefEntry = errors.New("couldn't get contentType of referenced entry")
ErrNoTypeOfRefAsset = errors.New("couldn't get contentType of referenced asset")
InfoUpdatedEntityCache = "updated cache for entity"
InfoCachedAllEntries = "cached all entries of content type"
InfoCachedAllAssets = "cached all assets"
InfoCachedAllTags = "cached all tags"
InfoFallingBackToFile = "gonna use a local file"
InfoLoadingFromFile = "loading space from local file"
InfoCacheIsNil = "contentful cache is nil"
InfoCacheWorkerStart = "contentful cache worker starting"
InfoCacheUpdateQueued = "contentful cache update queued"
InfoCacheUpdateCanceled = "contentful cache update canceled"
InfoCacheUpdateDone = "contentful cache update returning"
InfoCacheUpdateSkipped = "contentful cache update skipped, already one in the queue"
InfoCacheSyncOp = "contentful cache sync op done"
InfoOfflineEntitiesLoaded = "downloaded entries and assets from offline file"
InfoPreservingExistingCache = "could not connect for cache update, preserving the existing cache"
InfoUpdateCacheTime = "space caching done, time recorded"
ErrorEnvironmentSetToMaster = "environment was empty string, set to master"
ErrorEntryIsNil = "entry is nil"
ErrorEntrySysIsNil = "entry.Sys is nil"
ErrorEntryNotFound = "entry not found"
ErrorEntrySysContentTypeIsNil = "entry.Sys.ContentType is nil"
ErrorEntrySysContentTypeSysIsNil = "entry.Sys.ContentType.Sys is nil"
ErrorEntryCachingFailed = "entry caching failed"
)
var spaceContentTypes = []string{ContentTypeBrand, ContentTypeCategory, ContentTypeProduct}
var SpaceContentTypeInfoMap = ContentTypeInfoMap{
"brand": ContentTypeInfo{
ContentType: "brand",
Title: "Brand",
Description: "",
},
"category": ContentTypeInfo{
ContentType: "category",
Title: "Category",
Description: "",
},
"product": ContentTypeInfo{
ContentType: "product",
Title: "Product",
Description: "",
},
}
func (cc *ContentfulClient) BrokenReferences() (brokenReferences []BrokenReference) {
if cc.Cache == nil || cc.cacheMutex == nil {
return
}
cc.cacheMutex.parentMapGcLock.Lock()
defer cc.cacheMutex.parentMapGcLock.Unlock()
cc.cacheMutex.idContentTypeMapGcLock.Lock()
defer cc.cacheMutex.idContentTypeMapGcLock.Unlock()
for childID, parents := range cc.Cache.parentMap {
if _, okGotEntry := cc.Cache.idContentTypeMap[childID]; !okGotEntry {
for _, parent := range parents {
brokenReferences = append(brokenReferences, BrokenReference{
ParentID: parent.ID,
ParentType: parent.ContentType,
FromField: parent.FromField,
ChildID: childID,
})
}
}
}
return
}
func (cc *ContentfulClient) CacheHasContentType(contentTypeID string) bool {
if cc.Cache == nil {
return false
}
cc.cacheMutex.sharedDataGcLock.RLock()
defer cc.cacheMutex.sharedDataGcLock.RUnlock()
for _, cachedContentType := range cc.Cache.contentTypes {
if cachedContentType == contentTypeID {
return true
}
}
return false
}
func (cc *ContentfulClient) ClientStats() {
if cc == nil {
cc.logFn(nil, LogWarn, "ClientStats: no client available")
return
}
cc.cacheMutex.sharedDataGcLock.RLock()
defer cc.cacheMutex.sharedDataGcLock.RUnlock()
if cc.logFn != nil {
fieldsMap := map[string]interface{}{
"space ID": cc.SpaceID,
"environment": cc.Client.Environment,
"clientMode": cc.clientMode,
"contentTypes": strings.Join(cc.Cache.contentTypes, ","),
"locales": cc.locales,
"cached": cc.cacheInit,
}
if cc.cacheInit {
fieldsMap["cache asset count"] = len(cc.Cache.assets)
fieldsMap["cache entry count"] = len(cc.Cache.idContentTypeMap)
fieldsMap["cache parentMap children"] = len(cc.Cache.parentMap)
cc.cacheMutex.parentMapGcLock.RLock()
defer cc.cacheMutex.parentMapGcLock.RUnlock()
referenceCount := 0
for _, parents := range cc.Cache.parentMap {
referenceCount += len(parents)
}
fieldsMap["cache parentMap parents"] = referenceCount
cc.cacheMutex.genericEntriesGcLock.RLock()
defer cc.cacheMutex.genericEntriesGcLock.RUnlock()
fieldsMap["cache genericEntries"] = len(cc.Cache.genericEntries)
}
cc.logFn(fieldsMap, LogInfo, "Contentful ClientStats")
}
}
func (ref ContentfulReferencedEntry) ContentType() (contentType string) {
return ref.Entry.Sys.ContentType.Sys.ID
}
func (cc *ContentfulClient) UpsertAsset(ctx context.Context, asset *contentful.Asset) error {
if cc == nil || cc.Client == nil {
return errors.New("UpsertAsset: No client available")
}
if cc.clientMode != ClientModeCMA {
return errors.New("UpsertAsset: Only available in ClientModeCMA")
}
if asset.Fields != nil && asset.Fields.File != nil {
for _, locale := range SpaceLocales {
if asset.Fields.File[string(locale)] != nil {
asset.Fields.File[string(locale)].URL = strings.ReplaceAll(asset.Fields.File[string(locale)].URL, "https:", "")
}
}
}
errUpsert := cc.Client.Assets.Upsert(ctx, cc.SpaceID, asset)
if errUpsert != nil && !strings.Contains(errUpsert.Error(), "Not upserted") {
return errUpsert
}
return nil
}
func (cc *ContentfulClient) PublishAsset(ctx context.Context, asset *contentful.Asset) error {
if cc == nil || cc.Client == nil {
return errors.New("PublishAsset: No client available")
}
if cc.clientMode != ClientModeCMA {
return errors.New("PublishAsset: Only available in ClientModeCMA")
}
errPublish := cc.Client.Assets.Publish(ctx, cc.SpaceID, asset)
if errPublish != nil && !strings.Contains(errPublish.Error(), "Not published") {
return errPublish
}
return nil
}
func GetAssetPublishingStatus(asset *contentful.Asset) string {
if asset == nil {
return ""
}
if asset.Sys.PublishedVersion == 0 {
return StatusDraft
}
if asset.Sys.Version-asset.Sys.PublishedVersion == 1 {
return StatusPublished
}
return StatusChanged
}
func (cc *ContentfulClient) UpdateAsset(ctx context.Context, asset *contentful.Asset) (err error) {
if asset == nil {
return errors.New("UpdateAsset: Generic Entry is nil")
}
if cc == nil {
return errors.New("UpdateAsset: Generic Entry has nil Contentful client")
}
if cc.clientMode != ClientModeCMA {
return errors.New("UpdateAsset: Generic Entry not in ClientModeCMA")
}
publishingStatus := GetAssetPublishingStatus(asset)
err = cc.UpsertAsset(ctx, asset)
if err != nil {
return fmt.Errorf("UpdateAsset: upsert operation failed: %w", err)
}
if publishingStatus == StatusPublished {
err = cc.PublishAsset(ctx, asset)
if err != nil {
return fmt.Errorf("UpdateAsset: publish operation failed: %w", err)
}
}
return
}
func (cc *ContentfulClient) DeleteAsset(ctx context.Context, asset *contentful.Asset) error {
if cc == nil || cc.Client == nil {
return errors.New("DeleteAsset: No client available")
}
if cc.clientMode != ClientModeCMA {
return errors.New("DeleteAsset: Only available in ClientModeCMA")
}
errUnpublish := cc.Client.Assets.Unpublish(ctx, cc.SpaceID, asset)
if errUnpublish != nil && !strings.Contains(errUnpublish.Error(), "Not published") {
return errUnpublish
}
errDelete := cc.Client.Assets.Delete(ctx, cc.SpaceID, asset)
if errDelete != nil {
return errDelete
}
return nil
}
func (cc *ContentfulClient) DeleteAssetFromCache(key string) error {
return cc.deleteAssetFromCache(key)
}
func (cc *ContentfulClient) DisableTextJanitor() {
cc.textJanitor = false
}
func (cc *ContentfulClient) EnableTextJanitor() {
cc.textJanitor = true
}
func (cc *ContentfulClient) GetAllAssets(ctx context.Context) (map[string]*contentful.Asset, error) {
return cc.getAllAssets(ctx, true)
}
func (cc *ContentfulClient) GetAssetsByTag(ctx context.Context, tagName string) (vos []*contentful.Asset, err error) {
if cc == nil || cc.Client == nil {
return nil, errors.New("GetAssetsByTag: No client available")
}
if !cc.cacheInit {
return nil, errors.New("GetAssetsByTag: only available with cache")
}
tags, err := cc.getAllTags(ctx, true)
if err != nil {
return nil, errors.New("GetAssetsByTag could not get tags from cache: " + err.Error())
}
cc.cacheMutex.assetsGcLock.RLock()
defer cc.cacheMutex.assetsGcLock.RUnlock()
if _, tagExists := tags[tagName]; !tagExists {
return nil, nil
}
tagID := tags[tagName]
for _, vo := range cc.Cache.assets {
for _, voTag := range vo.Metadata.Tags {
if voTag.Sys.ID == tagID {
vos = append(vos, vo)
}
}
}
return vos, nil
}
func (cc *ContentfulClient) GetAllTags(ctx context.Context) (map[string]string, error) {
return cc.getAllTags(ctx, true)
}
func (cc *ContentfulClient) GetAssetByID(ctx context.Context, id string, forceNoCache ...bool) (*contentful.Asset, error) {
if cc == nil || cc.Client == nil {
return nil, errors.New("GetAssetByID: No client available")
}
cc.cacheMutex.sharedDataGcLock.Lock()
cacheInit := cc.cacheInit
cc.cacheMutex.sharedDataGcLock.Unlock()
cc.cacheMutex.assetsGcLock.Lock()
defer cc.cacheMutex.assetsGcLock.Unlock()
if cacheInit && cc.Cache.assets != nil && (len(forceNoCache) == 0 || !forceNoCache[0]) {
asset, okAsset := cc.Cache.assets[id]
if okAsset {
return asset, nil
} else {
return nil, errors.New("GetAssetByID: not found")
}
}
col := cc.Client.Assets.List(ctx, cc.SpaceID)
col.Query.Locale("*").Equal("sys.id", id)
_, err := col.Next()
if err != nil {
return nil, err
}
if len(col.Items) == 0 {
return nil, errors.New("GetAssetByID: Not found " + id)
}
item := col.Items[0]
asset := contentful.Asset{}
byt, err := json.Marshal(item)
if err != nil {
return nil, err
}
err = json.Unmarshal(byt, &asset)
if err != nil {
return nil, err
}
for _, loc := range []Locale{SpaceLocaleGerman, SpaceLocaleFrench} {
if _, ok := asset.Fields.File[string(loc)]; ok {
asset.Fields.File[string(loc)].URL = "https:" + asset.Fields.File[string(loc)].URL
}
}
return &asset, nil
}
func (cc *ContentfulClient) GetContentTypeOfID(ctx context.Context, id string) (string, error) {
if cc == nil || cc.Client == nil {
return "", errors.New("GetContentTypeOfID: No client available")
}
cc.cacheMutex.sharedDataGcLock.RLock()
cacheInit := cc.cacheInit
cc.cacheMutex.sharedDataGcLock.RUnlock()
if cacheInit {
okVo := false
cc.cacheMutex.brandGcLock.Lock()
_, okVo = cc.Cache.entryMaps.brand[id]
cc.cacheMutex.brandGcLock.Unlock()
if okVo {
return ContentTypeBrand, nil
}
cc.cacheMutex.categoryGcLock.Lock()
_, okVo = cc.Cache.entryMaps.category[id]
cc.cacheMutex.categoryGcLock.Unlock()
if okVo {
return ContentTypeCategory, nil
}
cc.cacheMutex.productGcLock.Lock()
_, okVo = cc.Cache.entryMaps.product[id]
cc.cacheMutex.productGcLock.Unlock()
if okVo {
return ContentTypeProduct, nil
}
return "", fmt.Errorf("GetContentTypeOfID: %s Not found in cache", id)
}
col := cc.Client.Entries.List(ctx, cc.SpaceID)
col.Query.Include(0).Equal("sys.id", id)
_, err := col.GetAll()
if err != nil {
return "", errors.New("GetContentTypeOfID: " + err.Error())
}
if len(col.Items) == 0 {
return "", fmt.Errorf("GetContentTypeOfID: %s Not found online", id)
}
var vo genericEntryNoFields
byteArray, _ := json.Marshal(col.Items[0])
err = json.NewDecoder(bytes.NewReader(byteArray)).Decode(&vo)
if err != nil {
return "", errors.New("GetContentTypeOfID: " + err.Error())
}
return vo.Sys.ContentType.Sys.ID, nil
}
func (cc *ContentfulClient) Locales() []Locale {
return cc.locales
}
func (cc *ContentfulClient) LocalesAsStrings() []string {
locales := []string{}
for _, locale := range cc.locales {
locales = append(locales, string(locale))
}
return locales
}
func (vo *GenericEntry) GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error) {
if vo == nil {
return nil, errors.New("GetParents: Value Object is nil")
}
if vo.CC == nil {
return nil, errors.New("GetParents: Value Object has no Contentful Client set")
}
return commonGetParents(ctx, vo.CC, vo.Sys.ID, contentType)
}
func (ref EntryReference) GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error) {
if ref.ID == "" {
return nil, errors.New("GetParents: reference is nil")
}
var candidateParents []EntryReference
var cc *ContentfulClient
if ref.CC != nil {
cc = ref.CC
} else if ref.VO != nil {
v := reflect.ValueOf(ref.VO)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil, errors.New("GetParents: no client available, tried reflection to no avail")
}
v = v.Elem()
}
field := v.FieldByName("CC")
if !field.IsValid() {
return nil, errors.New("GetParents: VO has no CC field")
}
if field.Kind() != reflect.Ptr {
return nil, errors.New("GetParents: VO field CC is not a pointer")
}
if field.IsNil() {
return nil, errors.New("GetParents: VO field CC is nil")
}
var ok bool
cc, ok = field.Interface().(*ContentfulClient)
if !ok {
return nil, errors.New("GetParents: VO field CC is not of type *ContentfulClient")
}
}
if cc == nil {
return nil, errors.New("GetParents: contentful client is nil")
}
if cc.Cache == nil {
return nil, errors.New("GetParents: only available in cached mode")
}
if len(contentType) == 0 {
return cc.Cache.parentMap[ref.ID], nil
} else {
cType := contentType[0]
for _, candidateParent := range cc.Cache.parentMap[ref.ID] {
if candidateParent.ContentType == cType {
candidateParents = append(candidateParents, candidateParent)
}
}
return candidateParents, nil
}
}
func GetOrInheritFieldValue(ctx context.Context, contentfulShop *ContentfulClient, entryID string, field string, parentContentTypes []string, locale Locale) (any, error) {
entry, err := contentfulShop.GetGenericEntry(entryID)
if err != nil {
return nil, fmt.Errorf("failed to get entry %s: %v", entryID, err)
}
// Try to get the field value from the current entry first
val, err := entry.FieldAsAny(field, locale)
if err == nil {
return val, nil
}
return inheritFieldValueRecursive(ctx, contentfulShop, entry, field, parentContentTypes, locale, make(map[string]bool))
}
func inheritFieldValueRecursive(ctx context.Context, contentfulShop *ContentfulClient, entry *GenericEntry, field string, parentContentTypes []string, locale Locale, visited map[string]bool) (any, error) {
if visited[entry.Sys.ID] {
return nil, fmt.Errorf("circular reference detected for entry %s", entry.Sys.ID)
}
visited[entry.Sys.ID] = true
parentRefs, err := commonGetParents(ctx, entry.CC, entry.Sys.ID, parentContentTypes)
if err != nil {
return nil, fmt.Errorf("failed to get parents for entry %s: %v", entry.Sys.ID, err)
}
for _, parentRef := range parentRefs {
parentEntry, err := entry.CC.GetGenericEntry(parentRef.ID)
if err != nil {
return nil, fmt.Errorf("failed to get parent entry %s: %v", parentRef.ID, err)
}
val, err := parentEntry.FieldAsAny(field, locale)
if err == nil {
return val, nil
}
inheritedVal, err := inheritFieldValueRecursive(ctx, contentfulShop, parentEntry, field, parentContentTypes, locale, visited)
if err == nil {
return inheritedVal, nil
}
}
return nil, ErrNotSet
}
func HtmlToRichText(htmlSrc string) *RichTextNode {
htmlClean := strings.TrimSpace(htmlSrc)
htmlClean = strings.ReplaceAll(htmlClean, ` `, ` `)
htmlClean = strings.ReplaceAll(htmlClean, "&", "&")
htmlClean = strings.ReplaceAll(htmlClean, "\t", "")
htmlClean = regexp.MustCompile(`[\s]{2,}`).ReplaceAllString(htmlClean, " ")
re := regexp.MustCompile(`>([^\n])`)
htmlClean = re.ReplaceAllString(htmlClean, fmt.Sprintf(">\n"+`$1`))
re = regexp.MustCompile(`(.{1})<`)
htmlClean = re.ReplaceAllString(htmlClean, `$1`+"\n<")
htmlLines := strings.Split(htmlClean, "\n")
rtnd := &RichTextNode{
NodeType: RichTextNodeDocument,
}
var isBasic bool
rtnd.Content, _, isBasic = richTextHtmlLinesToNode(htmlLines, 0, "", nil, true)
if isBasic {
basicRich := &RichTextNode{
NodeType: RichTextNodeDocument,
Content: rtnd.Content,
}
return basicRich
}
return rtnd
}
func NewAssetFromURL(id string, uploadUrl string, imageFileType string, title string, locale ...Locale) *contentful.Asset {
autoID := fmt.Sprintf("%d", time.Now().UnixNano()/int64(time.Millisecond))
if id == "" {
id = autoID
}
if title == "" {
title = id
}
var loc Locale
if len(locale) != 0 {
loc = locale[0]
if _, ok := localeFallback[loc]; !ok {
return nil
}
} else {
loc = DefaultLocale
}
asset := &contentful.Asset{
Sys: &contentful.Sys{
ID: id,
},
Fields: &contentful.FileFields{
File: map[string]*contentful.File{
string(loc): {
UploadURL: uploadUrl,
ContentType: imageFileType,
Name: id,
},
},
Title: map[string]string{
string(loc): title,
},
},
}
return asset
}
func NewContentfulClient(ctx context.Context, spaceID string, clientMode ClientMode, clientKey string, optimisticPageSize uint16, logFn func(fields map[string]interface{}, level int, args ...interface{}), logLevel int, debug bool) (*ContentfulClient, error) {
if spaceID == "" {
return nil, errors.New("NewContentfulClient: SpaceID cannot be empty")
}
if clientMode != ClientModeCMA && clientMode != ClientModeCPA && clientMode != ClientModeCDA {
return nil, errors.New("NewContentfulClient: clientMode not supported")
}
if optimisticPageSize < 10 {
return nil, errors.New("NewContentfulClient: optimisticPageSize must be 10 or bigger")
}
if logLevel < 0 || logLevel > 3 {
return nil, errors.New("NewContentfulClient: logLevel must be between 0 and 3")
}
if clientKey == "" {
return nil, errors.New("NewContentfulClient: Please provide an API key")
}
apiClient, err := getContentfulAPIClient(clientMode, clientKey)
if err != nil {
return nil, err
}
apiClient.Debug = debug
cc := &ContentfulClient{
clientMode: clientMode,
Client: apiClient,
Cache: &ContentfulCache{
assets: assetCacheMap{},
contentTypes: []string{},
entryMaps: cacheEntryMaps{},
genericEntries: map[string]*GenericEntry{},
idContentTypeMap: map[string]string{},
parentMap: map[string][]EntryReference{},
},
cacheMutex: &ContentfulCacheMutex{},
cacheQueue: make(chan struct{}, 1),
cacheDone: make(chan struct{}, 1),
cacheUpdateTimeout: 120,
locales: []Locale{SpaceLocaleGerman, SpaceLocaleFrench},
logFn: logFn,
logLevel: logLevel,
optimisticPageSize: optimisticPageSize,
SpaceID: spaceID,
sync: false,
}
_, err = cc.Client.Spaces.Get(ctx, spaceID)
if err != nil {
_, ok := err.(contentful.NotFoundError)
if ok {
return nil, errors.New("NewContentfulClient: That is not the space you're looking for")
}
return nil, errors.New("NewContentfulClient: " + err.Error())
}
return cc, nil
}
func NewOfflineContentfulClient(file []byte, logFn func(fields map[string]interface{}, level int, args ...interface{}), logLevel int, cacheAssets bool, textJanitor bool) (*ContentfulClient, error) {
offlineTemp, err := getOfflineSpaceFromFile(file)
if err != nil {
return nil, fmt.Errorf("NewOfflineContentfulClient could not parse space export file: %v", err)
}
cc := &ContentfulClient{
clientMode: ClientModeCDA,
Client: contentful.NewCDA(""),
Cache: &ContentfulCache{
contentTypes: []string{},
idContentTypeMap: map[string]string{},
genericEntries: map[string]*GenericEntry{},
parentMap: map[string][]EntryReference{},
},
cacheMutex: &ContentfulCacheMutex{},
cacheQueue: make(chan struct{}, 1),
cacheDone: make(chan struct{}, 1),
cacheUpdateTimeout: 120,
locales: []Locale{
SpaceLocaleGerman,
SpaceLocaleFrench,
},
logFn: logFn,
logLevel: logLevel,
SpaceID: "OFFLINE",
offline: true,
offlineTemp: *offlineTemp,
textJanitor: textJanitor,
}
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"entries": len(offlineTemp.Entries), "assets": len(offlineTemp.Assets)}, LogInfo, InfoLoadingFromFile)
}
_, _, err = cc.UpdateCache(context.Background(), spaceContentTypes, cacheAssets)
if err != nil {
return nil, fmt.Errorf("NewOfflineContentfulClient could not cache offline space: %v", err)
}
return cc, nil
}
func RichTextToHtml(rt interface{}, linkResolver LinkResolverFunc, entryLinkResolver EntryLinkResolverFunc, imageResolver ImageResolverFunc, embeddedEntryResolver EmbeddedEntryResolverFunc, locale Locale) (string, error) {
w := bytes.NewBuffer([]byte{})
node := &RichTextGenericNode{}
byt, err := json.Marshal(rt)
if err != nil {
return "", err
}
err = json.Unmarshal(byt, node)
if err != nil {
return "", err
}
err = node.richTextRenderHTML(w, linkResolver, entryLinkResolver, imageResolver, embeddedEntryResolver, locale)
if err != nil {
return "", err
}
out := w.String()
if out == "<p></p>" {
return "", nil
}
return out, nil
}
func RichTextToPlainText(rt interface{}, locale Locale) (string, error) {
htmlStr, err := RichTextToHtml(rt, nil, nil, nil, nil, locale)
if err != nil {
return "", err
}
doc, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
return "", err
}
var f func(*html.Node)
text := ""
f = func(n *html.Node) {
if n.Type == html.TextNode {
text += n.Data
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
f(doc)
return strings.TrimSpace(text), nil
}
func (cc *ContentfulClient) GetGenericEntry(entryID string) (*GenericEntry, error) {
if cc.Cache == nil {
if cc.logFn != nil && cc.logLevel <= LogWarn {
cc.logFn(map[string]interface{}{"task": "GetGenericEntry", "entryId": entryID}, LogWarn, InfoCacheIsNil)
}
return nil, errors.New(InfoCacheIsNil)
}
if _, ok := cc.Cache.genericEntries[entryID]; !ok {
if cc.logFn != nil && cc.logLevel <= LogWarn {
cc.logFn(map[string]interface{}{"task": "GetGenericEntry", "entryId": entryID}, LogWarn, ErrorEntryNotFound)
}
return nil, errors.New(ErrorEntryNotFound)
}
return cc.Cache.genericEntries[entryID], nil
}
func (cc *ContentfulClient) GetAllGenericEntries() (map[string]*GenericEntry, error) {
if cc.Cache == nil {
if cc.logFn != nil && cc.logLevel <= LogWarn {
cc.logFn(map[string]interface{}{"task": "GetAllGenericEntries"}, LogWarn, InfoCacheIsNil)
}
return nil, errors.New(InfoCacheIsNil)
}
return cc.Cache.genericEntries, nil
}
func (genericEntry *GenericEntry) FieldAsString(fieldName string, locale ...Locale) (string, error) {
var loc Locale
if len(locale) != 0 {
loc = locale[0]
if _, ok := localeFallback[loc]; !ok {
return "", ErrLocaleUnsupported
}
} else {
loc = DefaultLocale
}
if _, ok := genericEntry.RawFields[fieldName]; !ok {
return "", ErrNotSet
}
switch field := genericEntry.RawFields[fieldName].(type) {
case map[string]interface{}:
var fieldLoc any
if _, ok := field[string(loc)]; ok {
fieldLoc = field[string(loc)]
} else {
fieldLoc = field[string(DefaultLocale)]
}
switch fieldLocStr := fieldLoc.(type) {
case string:
return fieldLocStr, nil
default:
return "", ErrNotSet
}
default:
return "", ErrNotSet
}
}
func (genericEntry *GenericEntry) InheritAsString(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (string, error) {
val, err := genericEntry.FieldAsString(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return "", err
}
if len(parentRefs) > 0 {
for _, parentRef := range parentRefs {
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
if err != nil {
return "", err
}
parentVal, err := genericParent.FieldAsString(fieldName, locale...)
if err != nil {
return "", err
} else {
return parentVal, nil
}
}
}
return "", ErrNotSet
}
func (genericEntry *GenericEntry) FieldAsStringSlice(fieldName string, locale ...Locale) ([]string, error) {
var loc Locale
if len(locale) != 0 {
loc = locale[0]
if _, ok := localeFallback[loc]; !ok {
return nil, ErrLocaleUnsupported
}
} else {
loc = DefaultLocale
}
if _, ok := genericEntry.RawFields[fieldName]; !ok {
return nil, ErrNotSet
}
switch field := genericEntry.RawFields[fieldName].(type) {
case map[string]any:
var fieldLoc any
if _, ok := field[string(loc)]; ok {
fieldLoc = field[string(loc)]
} else {
fieldLoc = field[string(DefaultLocale)]
}
if fieldLoc != nil {
switch fieldLoc.(type) {
case []any:
var out []string
for _, v := range fieldLoc.([]any) {
switch v.(type) {
case string:
out = append(out, v.(string))
}
}
return out, nil
}
}
}
return nil, ErrNotSet
}
func (genericEntry *GenericEntry) InheritAsStringSlice(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) ([]string, error) {
val, err := genericEntry.FieldAsStringSlice(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return nil, err
}
if len(parentRefs) > 0 {
for _, parentRef := range parentRefs {
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
if err != nil {
return nil, err
}
parentVal, err := genericParent.FieldAsStringSlice(fieldName, locale...)
if err != nil {
return nil, err
} else {
return parentVal, nil
}
}
}
return nil, ErrNotSet
}
func (genericEntry *GenericEntry) FieldAsFloat64(fieldName string, locale ...Locale) (float64, error) {
var loc Locale
if len(locale) != 0 {
loc = locale[0]
if _, ok := localeFallback[loc]; !ok {
return 0, ErrLocaleUnsupported
}
} else {
loc = DefaultLocale
}
if _, ok := genericEntry.RawFields[fieldName]; !ok {
return 0, ErrNotSet
}
switch field := genericEntry.RawFields[fieldName].(type) {
case map[string]interface{}:
fieldLoc := field[string(loc)]
if fieldLoc == 0 && field[string(DefaultLocale)] != 0 {
fieldLoc = field[string(DefaultLocale)]
}
switch fieldLocFloat := fieldLoc.(type) {
case float64:
return fieldLocFloat, nil
default:
return 0, ErrNotSet
}
default:
return 0, ErrNotSet
}
}
func (genericEntry *GenericEntry) InheritAsFloat64(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (float64, error) {
val, err := genericEntry.FieldAsFloat64(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return 0, err
}
if len(parentRefs) > 0 {
for _, parentRef := range parentRefs {
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
if err != nil {
return 0, err
}
parentVal, err := genericParent.FieldAsFloat64(fieldName, locale...)
if err != nil {
return 0, err
} else {
return parentVal, nil
}
}
}
return 0, ErrNotSet
}
func (genericEntry *GenericEntry) FieldAsReference(fieldName string, locale ...Locale) (*EntryReference, error) {
var loc Locale
if len(locale) != 0 {
loc = locale[0]
if _, ok := localeFallback[loc]; !ok {
return nil, ErrLocaleUnsupported
}
} else {
loc = DefaultLocale
}
var cts ContentTypeSys
if _, ok := genericEntry.RawFields[fieldName]; !ok {
return nil, ErrNotSet
}
switch field := genericEntry.RawFields[fieldName].(type) {
case map[string]interface{}:
fieldVal := field[string(loc)]
if fieldVal == nil {
fieldVal = field[string(DefaultLocale)]
}
byt, err := json.Marshal(fieldVal)
if err != nil {
return nil, err
}
err = json.Unmarshal(byt, &cts)
if err != nil {
return nil, err
}
if cts.Sys.ID == "" {
return nil, errors.New("not a reference")
}
referencedEntry, err := genericEntry.CC.GetGenericEntry(cts.Sys.ID)
if err != nil || referencedEntry == nil {
return nil, err
}
ref := EntryReference{
ID: cts.Sys.ID,
ContentType: referencedEntry.Sys.ContentType.Sys.ID,
FromField: fieldName,
}
return &ref, nil
}
return nil, ErrNotSet
}
func (genericEntry *GenericEntry) InheritAsReference(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (*EntryReference, error) {
val, err := genericEntry.FieldAsReference(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return nil, err
}
if len(parentRefs) > 0 {
for _, parentRef := range parentRefs {
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
if err != nil {
return nil, err
}
parentVal, err := genericParent.FieldAsReference(fieldName, locale...)
if err != nil {
return nil, err
} else {
return parentVal, nil
}
}
}
return nil, ErrNotSet
}
func (genericEntry *GenericEntry) FieldAsAsset(ctx context.Context, fieldName string, locale ...Locale) (*contentful.AssetNoLocale, error) {
var loc Locale
reqLoc := DefaultLocale
if len(locale) != 0 {
loc = locale[0]
reqLoc = locale[0]
if _, ok := localeFallback[loc]; !ok {
return nil, ErrLocaleUnsupported
}
} else {
loc = DefaultLocale
}
var cts ContentTypeSys
if _, ok := genericEntry.RawFields[fieldName]; !ok {
return nil, ErrNotSet
}
switch field := genericEntry.RawFields[fieldName].(type) {
case map[string]interface{}:
fieldVal := field[string(loc)]
if fieldVal == nil {
fieldVal = field[string(DefaultLocale)]
}
byt, err := json.Marshal(fieldVal)
if err != nil {
return nil, err
}
err = json.Unmarshal(byt, &cts)
if err != nil {
return nil, err
}
asset, err := genericEntry.CC.GetAssetByID(ctx, cts.Sys.ID)
if err != nil {
if genericEntry.CC.logFn != nil && genericEntry.CC.logLevel == LogDebug {
genericEntry.CC.logFn(map[string]interface{}{"content type": genericEntry.Sys.ContentType.Sys.ID, "entry ID": genericEntry.Sys.ID, "method": "HeaderImage()"}, LogError, ErrNoTypeOfRefAsset)
}
return nil, ErrNotSet
}
tempAsset := &contentful.AssetNoLocale{}
tempAsset.Sys = asset.Sys
tempAsset.Fields = &contentful.FileFieldsNoLocale{}
if _, ok := asset.Fields.Title[string(reqLoc)]; ok {
tempAsset.Fields.Title = asset.Fields.Title[string(reqLoc)]
} else {
tempAsset.Fields.Title = asset.Fields.Title[string(loc)]
}
if _, ok := asset.Fields.Description[string(reqLoc)]; ok {
tempAsset.Fields.Description = asset.Fields.Description[string(reqLoc)]
} else {
tempAsset.Fields.Description = asset.Fields.Description[string(loc)]
}
if _, ok := asset.Fields.File[string(reqLoc)]; ok {
tempAsset.Fields.File = asset.Fields.File[string(reqLoc)]
} else {
tempAsset.Fields.File = asset.Fields.File[string(loc)]
}
return tempAsset, nil
}
return nil, ErrNotSet
}
func (genericEntry *GenericEntry) FieldAsMultipleReference(fieldName string, locale ...Locale) ([]*EntryReference, error) {
var loc Locale
if len(locale) != 0 {
loc = locale[0]
if _, ok := localeFallback[loc]; !ok {
return nil, ErrLocaleUnsupported
}
} else {
loc = DefaultLocale
}
var ctss []ContentTypeSys
if _, ok := genericEntry.RawFields[fieldName]; !ok {
return nil, ErrNotSet
}
switch field := genericEntry.RawFields[fieldName].(type) {
case map[string]interface{}:
fieldVal := field[string(loc)]
if fieldVal == nil {
fieldVal = field[string(DefaultLocale)]
}
byt, err := json.Marshal(fieldVal)
if err != nil {
return nil, err
}
err = json.Unmarshal(byt, &ctss)
if err != nil {
return nil, err
}
var refs []*EntryReference
for _, cts := range ctss {
if cts.Sys.ID == "" {
continue
}
referencedEntry, err := genericEntry.CC.GetGenericEntry(cts.Sys.ID)
if err != nil || referencedEntry == nil {
continue
}
refs = append(refs, &EntryReference{
ID: cts.Sys.ID,
ContentType: referencedEntry.Sys.ContentType.Sys.ID,
FromField: fieldName,
})
}
return refs, nil
}
return nil, ErrNotSet
}
func (genericEntry *GenericEntry) InheritAsMultipleReference(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) ([]*EntryReference, error) {
val, err := genericEntry.FieldAsMultipleReference(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return nil, err
}
if len(parentRefs) > 0 {
for _, parentRef := range parentRefs {
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
if err != nil {
return nil, err
}
parentVal, err := genericParent.FieldAsMultipleReference(fieldName, locale...)
if err != nil {
return nil, err
} else {
return parentVal, nil
}
}
}
return nil, ErrNotSet
}
func (genericEntry *GenericEntry) FieldAsBool(fieldName string, locale ...Locale) (bool, error) {
var loc Locale
if len(locale) != 0 {
loc = locale[0]
if _, ok := localeFallback[loc]; !ok {
return false, ErrLocaleUnsupported
}
} else {
loc = DefaultLocale
}
if _, ok := genericEntry.RawFields[fieldName]; !ok {
return false, ErrNotSet
}
switch field := genericEntry.RawFields[fieldName].(type) {
case map[string]interface{}:
var fieldLoc any
if _, ok := field[string(loc)]; ok {
fieldLoc = field[string(loc)]
} else {
fieldLoc = field[string(DefaultLocale)]
}
switch fieldLocStr := fieldLoc.(type) {
case bool:
return fieldLocStr, nil
default:
return false, ErrNotSet
}
default:
return false, ErrNotSet
}
}
func (genericEntry *GenericEntry) InheritAsBool(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (bool, error) {
val, err := genericEntry.FieldAsBool(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return false, err
}
if len(parentRefs) > 0 {
for _, parentRef := range parentRefs {
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
if err != nil {
return false, err
}
parentVal, err := genericParent.FieldAsBool(fieldName, locale...)
if err != nil {
return false, err
} else {
return parentVal, nil
}
}
}
return false, ErrNotSet
}
func (genericEntry *GenericEntry) FieldAsAny(fieldName string, locale ...Locale) (any, error) {
var loc Locale
if len(locale) != 0 {
loc = locale[0]
if _, ok := localeFallback[loc]; !ok {
return nil, ErrLocaleUnsupported
}
} else {
loc = DefaultLocale
}
if _, ok := genericEntry.RawFields[fieldName]; !ok {
return nil, ErrNotSet
}
switch field := genericEntry.RawFields[fieldName].(type) {
case map[string]interface{}:
var fieldLoc any
if _, ok := field[string(loc)]; ok {
fieldLoc = field[string(loc)]
} else {
fieldLoc = field[string(DefaultLocale)]
}
if fieldLoc != nil {
return fieldLoc, nil
}
}
return nil, ErrNotSet
}
func (genericEntry *GenericEntry) InheritAsAny(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (any, error) {
val, err := genericEntry.FieldAsAny(fieldName, locale...)
if err == nil {
return val, nil
}
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
if err != nil {
return nil, err
}
if len(parentRefs) > 0 {
for _, parentRef := range parentRefs {
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
if err != nil {
return nil, err
}
parentVal, err := genericParent.FieldAsAny(fieldName, locale...)
if err != nil {
return nil, err
} else {
return parentVal, nil
}
}
}
return nil, ErrNotSet
}
func (genericEntry *GenericEntry) SetField(fieldName string, fieldValue interface{}, locale ...Locale) error {
var loc Locale
if len(locale) != 0 {
loc = locale[0]
if _, ok := localeFallback[loc]; !ok {
return ErrLocaleUnsupported
}
} else {
loc = DefaultLocale
}
if genericEntry.RawFields == nil {
genericEntry.RawFields = make(RawFields)
}
if genericEntry.RawFields[fieldName] == nil {
genericEntry.RawFields[fieldName] = map[string]interface{}{}
}
switch genericEntry.RawFields[fieldName].(type) {
case map[string]interface{}:
genericEntry.RawFields[fieldName].(map[string]interface{})[string(loc)] = fieldValue
return nil
}
return ErrNotSet
}
func (genericEntry *GenericEntry) Upsert(ctx context.Context) error {
cfEntry := &contentful.Entry{
Fields: map[string]interface{}{},
}
// get the generic entry sys into the cfEntry sys
tmp, errMarshal := json.Marshal(genericEntry)
if errMarshal != nil {
return errors.New("can't marshal JSON from entry")
}
errUnmarshal := json.Unmarshal(tmp, &cfEntry)
if errUnmarshal != nil {
return errors.New("can't unmarshal JSON into CF entry")
}
// copy fields
for key, fieldValue := range genericEntry.RawFields {
cfEntry.Fields[key] = fieldValue
}
// upsert the entry
err := genericEntry.CC.Client.Entries.Upsert(ctx, genericEntry.CC.SpaceID, cfEntry)
if err != nil {
if genericEntry.CC.logFn != nil && genericEntry.CC.logLevel <= LogWarn {
genericEntry.CC.logFn(map[string]interface{}{"task": "UpdateCache"}, LogWarn, fmt.Errorf("CfAkeneoSettings UpsertEntry: Operation failed: %w", err))
}
return err
}
return nil
}
func (genericEntry *GenericEntry) Update(ctx context.Context) (err error) {
if genericEntry == nil {
return errors.New("Update: Generic Entry is nil")
}
if genericEntry.CC == nil {
return errors.New("Update: Generic Entry has nil Contentful client")
}
if genericEntry.CC.clientMode != ClientModeCMA {
return errors.New("Update: Generic Entry not in ClientModeCMA")
}
publishingStatus := genericEntry.GetPublishingStatus()
cfEntry := &contentful.Entry{}
tmp, errMarshal := json.Marshal(genericEntry)
if errMarshal != nil {
return errors.New("Update: Can't marshal JSON from VO")
}
errUnmarshal := json.Unmarshal(tmp, &cfEntry)
if errUnmarshal != nil {
return errors.New("Update: Can't unmarshal JSON into CF entry")
}
err = genericEntry.CC.Client.Entries.Upsert(ctx, genericEntry.CC.SpaceID, cfEntry)
if err != nil {
return fmt.Errorf("Update: upsert operation failed: %w", err)
}
tmp, errMarshal = json.Marshal(cfEntry)
if errMarshal != nil {
return errors.New("Update: Can't marshal JSON back from CF entry")
}
errUnmarshal = json.Unmarshal(tmp, &genericEntry)
if errUnmarshal != nil {
return errors.New("Update: Can't unmarshal JSON back into Generic Entry")
}
if publishingStatus == StatusPublished {
genericEntry.Sys.Version++
err = genericEntry.CC.Client.Entries.Publish(ctx, genericEntry.CC.SpaceID, cfEntry)
if err != nil {
return fmt.Errorf("Update: publish operation failed: %w", err)
}
}
return
}
func (genericEntry *GenericEntry) GetPublishingStatus() string {
if genericEntry == nil {
return ""
}
if genericEntry.Sys.PublishedVersion == 0 {
return StatusDraft
}
if genericEntry.Sys.Version-genericEntry.Sys.PublishedVersion == 1 {
return StatusPublished
}
return StatusChanged
}
func (cc *ContentfulClient) ClientMode() ClientMode {
return cc.clientMode
}
func (cc *ContentfulClient) SetCacheUpdateTimeout(seconds int64) {
cc.cacheUpdateTimeout = seconds
}
func (cc *ContentfulClient) SetEnvironment(environment string) {
if environment == "" {
cc.Client.Environment = "master"
if cc.logFn != nil && cc.logLevel <= LogWarn {
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogWarn, ErrorEnvironmentSetToMaster)
}
return
}
cc.Client.Environment = environment
}
func (cc *ContentfulClient) SetOfflineFallback(file []byte) error {
offlineTemp, err := getOfflineSpaceFromFile(file)
if err != nil {
return err
}
cc.offlineTemp = *offlineTemp
return nil
}
func (cc *ContentfulClient) SetSyncMode(mode bool) error {
cc.cacheMutex.sharedDataGcLock.Lock()
defer cc.cacheMutex.sharedDataGcLock.Unlock()
if cc.offline {
return errors.New("SetSyncMode: client is set offline, can't enable sync")
}
cc.sync = mode
return nil
}
func (cc *ContentfulClient) ResetSync() {
cc.cacheMutex.sharedDataGcLock.Lock()
defer cc.cacheMutex.sharedDataGcLock.Unlock()
cc.syncToken = ""
}
func (cc *ContentfulClient) UpdateCache(ctx context.Context, contentTypes []string, cacheAssets bool) (map[string][]string, []string, error) {
cc.cacheMutex.sharedDataGcLock.RLock()
ctxAtWork, cancel := context.WithTimeout(ctx, time.Second*time.Duration(cc.cacheUpdateTimeout))
defer cancel()
localOffline := cc.offline
isSync := cc.sync
cc.cacheMutex.sharedDataGcLock.RUnlock()
if localOffline {
ctxAtWork = ctx
}
if !localOffline {
time.Sleep(time.Second * 2)
}
if contentTypes == nil {
contentTypes = spaceContentTypes
} else {
for _, contentType := range contentTypes {
if !stringSliceContains(spaceContentTypes, contentType) {
return nil, nil, fmt.Errorf("UpdateCache: Content Type %q not available in this space", contentType)
}
}
}
tags, err := cc.GetAllTags(ctx)
if err != nil {
if cc.logFn != nil && cc.logLevel <= LogWarn {
cc.logFn(map[string]interface{}{"task": "UpdateCache", "error": err.Error()}, LogWarn, "failed to cache tags")
}
}
cc.cacheMutex.tagGcLock.Lock()
cc.Cache.tags = tags
cc.cacheMutex.tagGcLock.Unlock()
if isSync {
return cc.syncCache(ctxAtWork, contentTypes)
}
cc.cacheWorkerOnce.Do(func() {
go cc.cacheWorker(ctx, contentTypes, cacheAssets)
})
if len(cc.cacheQueue) == 0 {
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateQueued)
}
cc.cacheQueue <- struct{}{}
select {
case <-cc.cacheDone:
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateDone)
}
case <-ctxAtWork.Done():
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateCanceled)
}
}
cc.cacheMutex.sharedDataGcLock.Lock()
cc.cacheInit = true
cc.cacheMutex.sharedDataGcLock.Unlock()
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateDone)
}
return nil, nil, nil
}
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateSkipped)
}
return nil, nil, nil
}
func (cc *ContentfulClient) syncCache(ctx context.Context, contentTypes []string) (map[string][]string, []string, error) {
start := time.Now()
cc.cacheMutex.sharedDataGcLock.Lock()
cc.Cache.contentTypes = contentTypes
cc.cacheMutex.sharedDataGcLock.Unlock()
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "syncCache"}, LogInfo, InfoCacheUpdateQueued)
}
allTags, err := cc.getAllTags(ctx, false)
if err != nil {
return nil, nil, errors.New("syncCache failed for tags")
}
cc.cacheMutex.sharedDataGcLock.Lock()
cc.Cache.tags = allTags
cc.cacheMutex.sharedDataGcLock.Unlock()
var syncdEntries map[string][]string
var syncdAssets []string
for {
if ctx.Err() != nil {
return nil, nil, ctx.Err()
}
cc.cacheMutex.sharedDataGcLock.RLock()
col := cc.Client.Entries.Sync(
ctx,
cc.SpaceID,
cc.syncToken == "",
cc.syncToken,
)
cc.cacheMutex.sharedDataGcLock.RUnlock()
if _, err := col.GetAll(); err != nil {
return nil, nil, err
}
cc.cacheMutex.sharedDataGcLock.Lock()
cc.syncToken = col.SyncToken
cc.cacheInit = true
cc.cacheMutex.sharedDataGcLock.Unlock()
if len(col.Items) == 0 {
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"time elapsed": fmt.Sprint(time.Since(start)), "task": "syncCache"}, LogInfo, InfoUpdateCacheTime)
}
return syncdEntries, syncdAssets, nil
}
var entries []*contentful.Entry
var assets []*contentful.Asset
for _, item := range col.Items {
if ctx.Err() != nil {
return nil, nil, ctx.Err()
}
entry := &contentful.Entry{}
byteArray, _ := json.Marshal(item)
errEntry := json.NewDecoder(bytes.NewReader(byteArray)).Decode(entry)
if errEntry == nil && entry.Sys != nil && (entry.Sys.Type == sysTypeAsset || entry.Sys.Type == sysTypeDeletedAsset) {
asset := &contentful.Asset{}
errAsset := json.NewDecoder(bytes.NewReader(byteArray)).Decode(asset)
if errAsset != nil {
continue
}
if asset.Sys != nil {
assets = append(assets, asset)
}
continue
}
if entry.Sys != nil {
entries = append(entries, entry)
}
}
for _, entry := range entries {
if ctx.Err() != nil {
return nil, nil, ctx.Err()
}
switch entry.Sys.Type {
case sysTypeEntry:
if !stringSliceContains(spaceContentTypes, entry.Sys.ContentType.Sys.ID) {
continue
}
if err := updateCacheForContentTypeAndEntity(ctx, cc, entry.Sys.ContentType.Sys.ID, entry.Sys.ID, entry, false); err != nil {
if cc.logFn != nil && cc.logLevel <= LogWarn {
cc.logFn(map[string]interface{}{"id": entry.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to update cache for entry")
}
} else {
if syncdEntries == nil {
syncdEntries = map[string][]string{}
}
if _, ok := syncdEntries[entry.Sys.ContentType.Sys.ID]; !ok {
syncdEntries[entry.Sys.ContentType.Sys.ID] = []string{}
}
syncdEntries[entry.Sys.ContentType.Sys.ID] = append(syncdEntries[entry.Sys.ContentType.Sys.ID], entry.Sys.ID)
}
case sysTypeDeletedEntry:
cc.cacheMutex.idContentTypeMapGcLock.RLock()
contentType := cc.Cache.idContentTypeMap[entry.Sys.ID]
cc.cacheMutex.idContentTypeMapGcLock.RUnlock()
if err := updateCacheForContentTypeAndEntity(ctx, cc, contentType, entry.Sys.ID, entry, true); err != nil {
if cc.logFn != nil && cc.logLevel <= LogWarn {
cc.logFn(map[string]interface{}{"id": entry.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to delete cache for entry")
}
} else {
if syncdEntries == nil {
syncdEntries = map[string][]string{}
}
if _, ok := syncdEntries[contentType]; !ok {
syncdEntries[contentType] = []string{}
}
syncdEntries[contentType] = append(syncdEntries[contentType], entry.Sys.ID)
}
default:
}
}
if cc.logFn != nil && len(entries) > 0 && cc.logLevel <= LogInfo {
for contentType, ids := range syncdEntries {
cc.logFn(map[string]interface{}{"task": "syncCache", "contentType": contentType, "syncEntryCount": len(ids)}, LogInfo, InfoCacheSyncOp)
}
}
for _, asset := range assets {
if ctx.Err() != nil {
return nil, nil, ctx.Err()
}
switch asset.Sys.Type {
case sysTypeAsset:
if err := updateCacheForContentTypeAndEntity(ctx, cc, assetWorkerType, asset.Sys.ID, asset, false); err != nil {
if cc.logFn != nil && cc.logLevel <= LogWarn {
cc.logFn(map[string]interface{}{"id": asset.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to update cache for entry")
}
} else {
syncdAssets = append(syncdAssets, asset.Sys.ID)
}
case sysTypeDeletedAsset:
if err := updateCacheForContentTypeAndEntity(ctx, cc, assetWorkerType, asset.Sys.ID, nil, true); err != nil {
if cc.logFn != nil && cc.logLevel <= LogWarn {
cc.logFn(map[string]interface{}{"id": asset.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to delete cache for entry")
}
} else {
syncdAssets = append(syncdAssets, asset.Sys.ID)
}
default:
}
}
if cc.logFn != nil && len(assets) > 0 && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "syncCache", "syncAssetCount": len(syncdAssets)}, LogInfo, InfoCacheSyncOp)
}
}
return syncdEntries, syncdAssets, nil
}
func (cc *ContentfulClient) cacheWorker(ctx context.Context, contentTypes []string, cacheAssets bool) {
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheWorkerStart)
}
for range cc.cacheQueue {
cc.cacheSpace(ctx, contentTypes, cacheAssets)
cc.cacheDone <- struct{}{}
}
}
func (cc *ContentfulClient) cacheSpace(ctx context.Context, contentTypes []string, cacheAssets bool) {
start := time.Now()
tempCache := &ContentfulCache{
contentTypes: contentTypes,
idContentTypeMap: map[string]string{},
genericEntries: map[string]*GenericEntry{},
parentMap: map[string][]EntryReference{},
}
if cacheAssets {
contentTypes = append([]string{assetWorkerType, tagWorkerType}, contentTypes...)
}
_, errCanWeEvenConnect := cc.Client.Spaces.Get(ctx, cc.SpaceID)
cc.cacheMutex.sharedDataGcLock.RLock()
offlinePreviousState := cc.offline
cc.cacheMutex.sharedDataGcLock.RUnlock()
if errCanWeEvenConnect != nil {
if len(cc.offlineTemp.Entries) > 0 && (cc.Cache == nil || offlinePreviousState) {
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "UpdateCache", "clientMode": cc.clientMode}, LogInfo, InfoFallingBackToFile)
}
cc.cacheMutex.sharedDataGcLock.Lock()
cc.offline = true
cc.cacheMutex.sharedDataGcLock.Unlock()
} else {
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "UpdateCache", "clientMode": cc.clientMode}, LogInfo, InfoPreservingExistingCache)
}
}
}
results := make(chan ContentTypeResult, 16)
resultsDone := make(chan struct{})
contentTypeChan := make(chan string)
group, gctx := errgroup.WithContext(ctx)
for i := 0; i < cacheUpdateConcurrency; i++ {
group.Go(func() error {
for contentType := range contentTypeChan {
err := updateCacheForContentType(gctx, results, cc, tempCache, contentType)
if err != nil {
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "UpdateCache", "contentType": contentType, "clientMode": cc.clientMode}, LogError, err.Error())
}
return err
}
}
return nil
})
}
go func() {
for _, contentType := range contentTypes {
contentTypeChan <- contentType
}
close(contentTypeChan)
}()
go func() {
for res := range results {
tempCache.idContentTypeMap[res.EntryID] = res.ContentType
for childID, references := range res.References {
tempCache.parentMap[childID] = append(tempCache.parentMap[childID], references...)
}
}
resultsDone <- struct{}{}
}()
err := group.Wait()
close(results)
if err != nil {
// drain contentTypeChan
for range contentTypeChan {
}
cc.cacheMutex.sharedDataGcLock.Lock()
cc.offline = offlinePreviousState
cc.cacheMutex.sharedDataGcLock.Unlock()
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "UpdateCache", "clientMode": cc.clientMode}, LogError, err.Error())
}
return
}
// Signal that the cache build is done
<-resultsDone
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"time elapsed": fmt.Sprint(time.Since(start)), "task": "UpdateCache", "clientMode": cc.clientMode}, LogInfo, InfoUpdateCacheTime)
}
cc.cacheMutex.fullCacheGcLock.Lock()
cc.cacheMutex.sharedDataGcLock.Lock()
cc.cacheMutex.assetsGcLock.Lock()
cc.cacheMutex.idContentTypeMapGcLock.Lock()
cc.cacheMutex.parentMapGcLock.Lock()
cc.cacheMutex.genericEntriesGcLock.Lock()
cc.cacheMutex.brandGcLock.Lock()
cc.cacheMutex.categoryGcLock.Lock()
cc.cacheMutex.productGcLock.Lock()
cc.Cache = tempCache
cc.offline = offlinePreviousState
cc.cacheMutex.brandGcLock.Unlock()
cc.cacheMutex.categoryGcLock.Unlock()
cc.cacheMutex.productGcLock.Unlock()
cc.cacheMutex.parentMapGcLock.Unlock()
cc.cacheMutex.idContentTypeMapGcLock.Unlock()
cc.cacheMutex.assetsGcLock.Unlock()
cc.cacheMutex.sharedDataGcLock.Unlock()
cc.cacheMutex.fullCacheGcLock.Unlock()
cc.cacheMutex.genericEntriesGcLock.Unlock()
}
func ToAssetReference(asset *contentful.Asset) (refSys ContentTypeSys) {
refSys.Sys.ID = asset.Sys.ID
refSys.Sys.Type = FieldTypeLink
refSys.Sys.LinkType = FieldLinkTypeAsset
return
}
func (cc *ContentfulClient) UpdateCacheForEntity(ctx context.Context, sysType string, contentType string, entityID string) error {
if sysType == sysTypeEntry && cc.entryMapForContentTypeIsNil(contentType) {
return fmt.Errorf("UpdateCacheForEntity: Content Type %q not available in cache", contentType)
}
if sysType == sysTypeAsset {
contentType = assetWorkerType
}
if contentType != assetWorkerType && !stringSliceContains(spaceContentTypes, contentType) {
return fmt.Errorf("UpdateCache: Content Type %q not available in this space", contentType)
}
return updateCacheForContentTypeAndEntity(ctx, cc, contentType, entityID, nil, false)
}
func FieldToObject(jsonField interface{}, targetObject interface{}) error {
byteArray, err := json.Marshal(jsonField)
if err != nil {
return err
}
err = json.Unmarshal(byteArray, &targetObject)
if err != nil {
return err
}
return nil
}
func (cc *ContentfulClient) cacheGcAssetByID(ctx context.Context, id string, asset *contentful.Asset) error {
if asset == nil {
if cc.Client == nil {
return errors.New("cacheGcAssetByID: No client available")
}
col := cc.Client.Assets.List(ctx, cc.SpaceID)
col.Query.Locale("*").Equal("sys.id", id)
_, err := col.Next()
if err != nil {
return err
}
if len(col.Items) == 0 {
return errors.New("cacheGcAssetByID: Not found " + id)
}
item := col.Items[0]
byt, err := json.Marshal(item)
if err != nil {
return err
}
err = json.Unmarshal(byt, &asset)
if err != nil {
return err
}
}
for _, loc := range []Locale{SpaceLocaleGerman, SpaceLocaleFrench} {
if _, ok := asset.Fields.File[string(loc)]; ok {
asset.Fields.File[string(loc)].URL = "https:" + asset.Fields.File[string(loc)].URL
}
}
cc.cacheMutex.assetsGcLock.Lock()
cc.Cache.assets[id] = asset
cc.cacheMutex.assetsGcLock.Unlock()
return nil
}
func (cc *ContentfulClient) deleteAssetFromCache(key string) error {
if cc.Cache == nil || cc.cacheMutex == nil {
return errors.New("no cache available")
}
cc.cacheMutex.assetsGcLock.Lock()
if _, ok := cc.Cache.assets[key]; ok {
delete(cc.Cache.assets, key)
cc.cacheMutex.assetsGcLock.Unlock()
return nil
}
cc.cacheMutex.assetsGcLock.Unlock()
return errors.New("asset not found in cache, could not delete")
}
func (cc *ContentfulClient) entryMapForContentTypeIsNil(contentType string) bool {
switch contentType {
case ContentTypeBrand:
if cc.Cache.entryMaps.brand == nil {
return true
}
case ContentTypeCategory:
if cc.Cache.entryMaps.category == nil {
return true
}
case ContentTypeProduct:
if cc.Cache.entryMaps.product == nil {
return true
}
}
return false
}
func getContentfulAPIClient(clientMode ClientMode, clientKey string) (*contentful.Contentful, error) {
switch clientMode {
case ClientModeCDA:
return contentful.NewCDA(clientKey), nil
case ClientModeCPA:
return contentful.NewCPA(clientKey), nil
case ClientModeCMA:
return contentful.NewCMA(clientKey), nil
default:
return nil, errors.New("NewContentfulClient: Unknown ClientMode")
}
}
func (cc *ContentfulClient) getAllAssets(ctx context.Context, tryCacheFirst bool) (map[string]*contentful.Asset, error) {
if cc == nil || cc.Client == nil {
return nil, errors.New("getAllAssets: No client available")
}
cc.cacheMutex.sharedDataGcLock.RLock()
offline := cc.offline
cacheInit := cc.cacheInit
cc.cacheMutex.sharedDataGcLock.RUnlock()
if cacheInit && cc.Cache.assets != nil && tryCacheFirst {
return cc.Cache.assets, nil
}
allItems := []interface{}{}
assets := map[string]*contentful.Asset{}
if offline {
for _, asset := range cc.offlineTemp.Assets {
allItems = append(allItems, asset)
}
} else {
col := cc.Client.Assets.List(ctx, cc.SpaceID)
col.Query.Locale("*").Limit(assetPageSize)
for {
_, err := col.Next()
if err != nil {
return nil, err
}
allItems = append(allItems, col.Items...)
if uint16(len(col.Items)) < assetPageSize {
break
}
}
}
for _, item := range allItems {
asset := contentful.Asset{}
byt, err := json.Marshal(item)
if err != nil {
return nil, err
}
err = json.Unmarshal(byt, &asset)
if err != nil {
return nil, err
}
for _, loc := range []Locale{SpaceLocaleGerman, SpaceLocaleFrench} {
if _, ok := asset.Fields.File[string(loc)]; ok {
asset.Fields.File[string(loc)].URL = "https:" + asset.Fields.File[string(loc)].URL
}
}
assets[asset.Sys.ID] = &asset
}
return assets, nil
}
func (cc *ContentfulClient) getAllTags(ctx context.Context, tryCacheFirst bool) (map[string]string, error) {
if cc == nil || cc.Client == nil {
return nil, errors.New("getAllTags: No client available")
}
cc.cacheMutex.sharedDataGcLock.RLock()
offline := cc.offline
cacheInit := cc.cacheInit
cc.cacheMutex.sharedDataGcLock.RUnlock()
cc.cacheMutex.tagGcLock.RLock()
defer cc.cacheMutex.tagGcLock.RUnlock()
if cacheInit && cc.Cache.tags != nil && tryCacheFirst {
return cc.Cache.tags, nil
}
allItems := []interface{}{}
tags := map[string]string{}
if offline {
for _, asset := range cc.offlineTemp.Tags {
allItems = append(allItems, asset)
}
} else {
col := cc.Client.Tags.List(ctx, cc.SpaceID)
col.Query.Limit(1000)
_, err := col.Next()
if err != nil {
return nil, err
}
allItems = col.Items
}
for _, item := range allItems {
tag := contentful.Tag{}
byt, err := json.Marshal(item)
if err != nil {
return nil, err
}
err = json.Unmarshal(byt, &tag)
if err != nil {
return nil, err
}
tags[tag.Name] = tag.Sys.ID
}
return tags, nil
}
func getOfflineSpaceFromFile(file []byte) (*offlineTemp, error) {
offlineTemp := &offlineTemp{}
err := json.Unmarshal(file, offlineTemp)
if err != nil {
return nil, fmt.Errorf("getOfflineSpaceFromFile could not parse space export file: %v", err)
}
return offlineTemp, nil
}
func (cc *ContentfulClient) optimisticPageSizeGetAll(ctx context.Context, contentType string, limit uint16) (*contentful.Collection, error) {
col := cc.Client.Entries.List(ctx, cc.SpaceID)
col.Query.ContentType(contentType).Locale("*").Include(0).Limit(limit)
allItems := []interface{}{}
var err error
for {
_, err = col.Next()
if err != nil {
break
}
allItems = append(allItems, col.Items...)
if uint16(len(col.Items)) < limit {
break
}
}
col.Items = allItems
switch errTyped := err.(type) {
case contentful.ErrorResponse:
msg := errTyped.Message
if (strings.Contains(msg, "Response size too big") || strings.Contains(msg, "Too many links")) && limit >= 20 {
smallerPageCol, err := cc.optimisticPageSizeGetAll(ctx, contentType, limit/2)
return smallerPageCol, err
}
return nil, err
case nil:
default:
return nil, err
}
return col, nil
}
func richTextGetAttribute(htmlLine string, attribute string) string {
re := regexp.MustCompile(` href=["']([^"']+)["']`)
matches := re.FindStringSubmatch(htmlLine)
if len(matches) == 2 {
return strings.ToLower(matches[1])
}
return ""
}
func richTextGetMark(mark string) string {
switch mark {
case HtmlItalic, HtmlEm:
return RichTextMarkItalic
case HtmlBold, HtmlStrong:
return RichTextMarkBold
case HtmlUnderline:
return RichTextMarkUnderline
case HtmlCode:
return RichTextMarkCode
}
return ""
}
func richTextHtmlLinesToNode(htmlLines []string, start int, pendingTag string, marks []string, isBasic bool) ([]interface{}, int, bool) {
nodeSlice := make([]interface{}, 0)
for i := start; i < len(htmlLines); i++ {
htmlLine := htmlLines[i]
if richTextIsHtmlClosingPending(htmlLine, pendingTag) {
return nodeSlice, i, isBasic
}
currentNode := RichTextNode{}
if richTextIsHtmlTag(htmlLine) {
tt := richTextHtmlTagType(htmlLine)
switch tt {
case HtmlParagraph, HtmlHeading1, HtmlHeading2, HtmlHeading3, HtmlHeading4, HtmlHeading5, HtmlHeading6, HtmlUnorderedList, HtmlOrderedList:
isBasic = false
currentNode.NodeType = richTextMapTagNodeType(tt)
var nextLine int
currentNode.Content, nextLine, isBasic = richTextHtmlLinesToNode(htmlLines, i+1, tt, marks, isBasic)
nodeSlice = append(nodeSlice, currentNode)
if nextLine == -1 {
return nodeSlice, -1, isBasic
}
i = nextLine
case HtmlBlockquote, HtmlListItem:
isBasic = false
currentNode.NodeType = richTextMapTagNodeType(tt)
currentNode.Content = make([]interface{}, 0)
innerContent, nextLine, isBasic := richTextHtmlLinesToNode(htmlLines, i+1, tt, marks, isBasic)
if len(innerContent) == 1 {
switch innerContent[0].(type) {
case RichTextNodeTextNode:
currentNode.Content = append(currentNode.Content, RichTextNode{
NodeType: RichTextNodeParagraph,
Content: innerContent,
})
}
} else {
for _, ct := range innerContent {
switch ct.(type) {
case RichTextNode:
currentNode.Content = append(currentNode.Content, ct)
case RichTextNodeTextNode:
currentNode.Content = append(currentNode.Content, RichTextNode{
NodeType: RichTextNodeParagraph,
Content: []interface{}{ct},
})
}
}
}
nodeSlice = append(nodeSlice, currentNode)
if nextLine == -1 {
return nodeSlice, -1, isBasic
}
i = nextLine
case HtmlCode:
isBasic = false
currentNode.NodeType = RichTextNodeParagraph
var nextLine int
currentNode.Content, nextLine, isBasic = richTextHtmlLinesToNode(htmlLines, i+1, HtmlCode, []string{HtmlCode}, isBasic)
nodeSlice = append(nodeSlice, currentNode)
if nextLine == -1 {
return nodeSlice, -1, isBasic
}
i = nextLine
case HtmlAnchor:
isBasic = false
currentNode.NodeType = RichTextNodeHyperlink
var nextLine int
anchorURI := richTextGetAttribute(htmlLine, HtmlAttributeHref)
if anchorURI == "" {
anchorURI = "/"
}
currentNode.Data = RichTextData{URI: anchorURI}
currentNode.Content, nextLine, isBasic = richTextHtmlLinesToNode(htmlLines, i+1, tt, marks, isBasic)
nodeSlice = append(nodeSlice, currentNode)
if nextLine == -1 {
return nodeSlice, -1, isBasic
}
i = nextLine
case HtmlHorizontalRule:
isBasic = false
currentNode.NodeType = RichTextNodeHR
currentNode.Content = make([]interface{}, 0)
nodeSlice = append(nodeSlice, currentNode)
case HtmlItalic, HtmlEm, HtmlBold, HtmlStrong, HtmlUnderline:
marks = append(marks, tt)
case HtmlBreak:
if len(nodeSlice) > 0 {
myNode := nodeSlice[len(nodeSlice)-1]
switch myNodeTyped := myNode.(type) {
case RichTextNodeTextNode:
if myNodeTyped.NodeType == RichTextNodeText {
myNodeTyped.Value += "\n"
nodeSlice[len(nodeSlice)-1] = myNodeTyped
}
}
}
}
continue //unsupported tags will be ignored but content is preserved
}
if richTextIsHtmlClosingTag(htmlLine) {
continue //closing tags that are not pending equal to unknown
}
if htmlLine == " " {
continue
}
currentNodeTextNode := RichTextNodeTextNode{}
currentNodeTextNode.NodeType = RichTextNodeText
currentNodeTextNode.Marks = []RichTextMark{}
currentNodeTextNode.Value = htmlLine
for _, mark := range marks {
currentNodeTextNode.Marks = append(currentNodeTextNode.Marks, RichTextMark{
Type: richTextGetMark(mark),
})
}
marks = nil
if pendingTag == "" {
nodeSlice = append(nodeSlice, RichTextNode{
NodeType: RichTextNodeParagraph,
Content: []interface{}{currentNodeTextNode},
})
} else {
nodeSlice = append(nodeSlice, currentNodeTextNode)
}
}
return nodeSlice, -1, isBasic
}
func richTextIsHtmlClosingPending(htmlLine, tag string) bool {
return regexp.MustCompile(`</` + strings.ToLower(tag) + `.+$`).MatchString(strings.ToLower(htmlLine))
}
func richTextIsHtmlClosingTag(htmlLine string) bool {
return regexp.MustCompile(`</[a-zA-Z]+.+$`).MatchString(htmlLine)
}
func richTextIsHtmlTag(htmlLine string) bool {
return regexp.MustCompile(`<[a-zA-Z]+.+$`).MatchString(htmlLine)
}
func richTextMapTagNodeType(tag string) string {
switch tag {
case HtmlParagraph:
return RichTextNodeParagraph
case HtmlAnchor:
return RichTextNodeHyperlink
case HtmlHeading1:
return RichTextNodeHeading1
case HtmlHeading2:
return RichTextNodeHeading2
case HtmlHeading3:
return RichTextNodeHeading3
case HtmlHeading4:
return RichTextNodeHeading4
case HtmlHeading5:
return RichTextNodeHeading5
case HtmlHeading6:
return RichTextNodeHeading6
case HtmlBlockquote:
return RichTextNodeBlockquote
case HtmlUnorderedList:
return RichTextNodeUnorderedList
case HtmlOrderedList:
return RichTextNodeOrderedList
case HtmlListItem:
return RichTextNodeListItem
}
return ""
}
func (ts richTextHtmlTags) richTextHtmlTagsOpen(w io.Writer) {
for _, t := range ts {
if t.customHTML != "" {
w.Write([]byte(t.customHTML))
continue
}
tagString := "<" + t.name
if len(t.attrs) > 0 {
for name, value := range t.attrs {
tagString += " " + name + `="` + html.EscapeString(value) + `"`
}
}
w.Write([]byte(tagString + ">"))
}
}
func (ts richTextHtmlTags) richTextHtmlTagsClose(w io.Writer) {
for _, t := range ts {
if t.customHTML != "" {
continue
}
w.Write([]byte("</" + t.name + ">"))
}
}
func richTextHtmlTagType(htmlLine string) string {
re := regexp.MustCompile(`^<([a-zA-Z0-9]+)[\s]*>*`)
matches := re.FindStringSubmatch(htmlLine)
if len(matches) == 2 {
return strings.ToLower(matches[1])
}
return ""
}
func (n *RichTextGenericNode) richTextRenderHTML(w io.Writer, linkResolver LinkResolverFunc, entryLinkResolver EntryLinkResolverFunc, imageResolver ImageResolverFunc, embeddedEntryResolver EmbeddedEntryResolverFunc, locale Locale) (err error) {
if linkResolver == nil {
linkResolver = func(url string) (transformedAttrs map[string]string, err error) {
return map[string]string{
"href": url,
}, nil
}
}
if entryLinkResolver == nil {
entryLinkResolver = func(entryID string, locale Locale) (transformedAttrs map[string]string, err error) {
return map[string]string{}, nil
}
}
if embeddedEntryResolver == nil {
embeddedEntryResolver = func(entryID string, locale Locale) (htmlSnippet string, err error) {
return "", nil
}
}
if imageResolver == nil {
imageResolver = func(assetID string, locale Locale) (attrs map[string]string, customHTML string, resolveError error) {
return map[string]string{}, "", nil
}
}
tags := richTextHtmlTags{}
switch n.NodeType {
case RichTextNodeParagraph:
tags = []richTextHtmlTag{{name: HtmlParagraph}}
case RichTextNodeHeading1:
tags = []richTextHtmlTag{{name: HtmlHeading1}}
case RichTextNodeHeading2:
tags = []richTextHtmlTag{{name: HtmlHeading2}}
case RichTextNodeHeading3:
tags = []richTextHtmlTag{{name: HtmlHeading3}}
case RichTextNodeHeading4:
tags = []richTextHtmlTag{{name: HtmlHeading4}}
case RichTextNodeHeading5:
tags = []richTextHtmlTag{{name: HtmlHeading5}}
case RichTextNodeHeading6:
tags = []richTextHtmlTag{{name: HtmlHeading6}}
case RichTextNodeOrderedList:
tags = []richTextHtmlTag{{name: HtmlOrderedList}}
case RichTextNodeUnorderedList:
tags = []richTextHtmlTag{{name: HtmlUnorderedList}}
case RichTextNodeListItem:
tags = []richTextHtmlTag{{name: HtmlListItem}}
case RichTextNodeHR:
tags = []richTextHtmlTag{{name: HtmlHorizontalRule}}
case RichTextNodeBlockquote:
tags = []richTextHtmlTag{{name: HtmlBlockquote}}
case RichTextNodeText:
tags = []richTextHtmlTag{}
case RichTextNodeHyperlink:
if n.Data != nil {
uri := n.Data["uri"]
attrs := map[string]string{}
switch uriString := uri.(type) {
case string:
if uriString != "" {
resolvedAttrs, errResolveAttrs := linkResolver(uriString)
if errResolveAttrs != nil {
err = errResolveAttrs
return
}
attrs = resolvedAttrs
}
}
tags = []richTextHtmlTag{richTextHtmlTag{name: HtmlAnchor, attrs: attrs}}
}
case RichTextNodeEntryHyperlink:
if n.Data != nil {
target := n.Data["target"]
attrs := map[string]string{}
switch target.(type) {
case map[string]interface{}:
targetSys, ok := target.(map[string]interface{})["sys"]
if ok {
entryID := targetSys.(map[string]interface{})["id"].(string)
resolvedAttrs, errResolveAttrs := entryLinkResolver(entryID, locale)
if errResolveAttrs != nil {
err = errResolveAttrs
return
}
attrs = resolvedAttrs
}
}
tags = []richTextHtmlTag{richTextHtmlTag{name: HtmlAnchor, attrs: attrs}}
}
case RichTextNodeEmbeddedAsset:
if imageResolver == nil {
return errors.New("can't resolve image asset URL")
}
dataObj := RichTextData{}
byt, err := json.Marshal(n.Data)
if err != nil {
return err
}
err = json.Unmarshal(byt, &dataObj)
if err != nil {
return err
}
if dataObj.Target == nil {
return errors.New("data target is empty")
}
attrs, customHTML, err := imageResolver(dataObj.Target.Sys.ID, locale)
if err != nil {
return err
}
tags = []richTextHtmlTag{richTextHtmlTag{name: HtmlImage, attrs: attrs, customHTML: customHTML}}
case RichTextNodeEmbeddedEntry:
dataObj := RichTextData{}
byt, errMarshal := json.Marshal(n.Data)
if errMarshal != nil {
return errMarshal
}
errUnmarshal := json.Unmarshal(byt, &dataObj)
if errUnmarshal != nil {
return errUnmarshal
}
if dataObj.Target == nil {
return errors.New("data target is empty")
}
rawHTML, errResolve := embeddedEntryResolver(dataObj.Target.Sys.ID, locale)
if errResolve != nil {
return errResolve
}
w.Write([]byte(rawHTML))
return
case RichTextNodeTable:
tags = []richTextHtmlTag{{name: HtmlTable}}
case RichTextNodeTableRow:
tags = []richTextHtmlTag{{name: HtmlTableRow}}
case RichTextNodeTableHeaderCell:
tags = []richTextHtmlTag{{name: HtmlTableHeaderCell}}
case RichTextNodeTableCell:
tags = []richTextHtmlTag{{name: HtmlTableCell}}
default:
}
for _, m := range n.Marks {
markTag := ""
switch m.Type {
case RichTextMarkBold:
markTag = HtmlBold
case RichTextMarkItalic:
markTag = HtmlItalic
case RichTextMarkUnderline:
markTag = HtmlUnderline
case RichTextMarkCode:
markTag = HtmlCode
case RichTextNodeBlockquote:
markTag = HtmlBlockquote
}
if markTag != "" {
tags = append(tags, richTextHtmlTag{name: markTag})
}
}
tags.richTextHtmlTagsOpen(w)
cleanString := strings.Replace(html.EscapeString(n.Value), "\n", "<br/>", -1)
cleanString = strings.ReplaceAll(cleanString, "\u00a0", " ")
w.Write([]byte(cleanString))
for _, subNode := range n.Content {
errSubNode := subNode.richTextRenderHTML(w, linkResolver, entryLinkResolver, imageResolver, embeddedEntryResolver, locale)
if errSubNode != nil {
err = errSubNode
return
}
}
tags.richTextHtmlTagsClose(w)
return
}
// MarshalJSON implements custom JSON marshalling for RichTextGenericNode for compatibility with Contentful's upsert API
func (n *RichTextGenericNode) MarshalJSON() ([]byte, error) {
if n == nil {
return []byte("null"), nil
}
if n.Data == nil {
n.Data = make(map[string]interface{})
}
if n.NodeType == "text" {
// For text nodes, include Data, Value and Marks, but not Content
return json.Marshal(&struct {
NodeType string `json:"nodeType"`
Data map[string]interface{} `json:"data"`
Value string `json:"value"`
Marks []RichTextMark `json:"marks"`
}{
NodeType: n.NodeType,
Data: n.Data,
Value: n.Value,
Marks: n.Marks,
})
} else {
// For non-text nodes, exclude Data and Marks
return json.Marshal(&struct {
NodeType string `json:"nodeType"`
Data map[string]interface{} `json:"data"`
Content []*RichTextGenericNode `json:"content"`
}{
NodeType: n.NodeType,
Content: n.Content,
Data: n.Data,
})
}
}
func stringSliceContains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func updateCacheForContentType(ctx context.Context, results chan ContentTypeResult, cc *ContentfulClient, tempCache *ContentfulCache, contentType string) error {
if ctx.Err() != nil {
return ctx.Err()
}
switch contentType {
case ContentTypeBrand:
allBrand, err := cc.cacheAllBrand(ctx, results)
if err != nil {
return errors.New("updateCacheForContentType failed for contentType brand: " + err.Error())
}
tempCache.entryMaps.brand = allBrand
cc.cacheMutex.genericEntriesGcLock.Lock()
for _, brand := range allBrand {
tempCache.genericEntries[brand.Sys.ID] = &GenericEntry{
Sys: brand.Sys,
RawFields: brand.RawFields,
CC: brand.CC,
}
}
cc.cacheMutex.genericEntriesGcLock.Unlock()
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"contentType": "brand", "method": "updateCacheForContentType", "clientMode": cc.clientMode, "size": len(allBrand)}, LogInfo, InfoCachedAllEntries)
}
case ContentTypeCategory:
allCategory, err := cc.cacheAllCategory(ctx, results)
if err != nil {
return errors.New("updateCacheForContentType failed for contentType category: " + err.Error())
}
tempCache.entryMaps.category = allCategory
cc.cacheMutex.genericEntriesGcLock.Lock()
for _, category := range allCategory {
tempCache.genericEntries[category.Sys.ID] = &GenericEntry{
Sys: category.Sys,
RawFields: category.RawFields,
CC: category.CC,
}
}
cc.cacheMutex.genericEntriesGcLock.Unlock()
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"contentType": "category", "method": "updateCacheForContentType", "clientMode": cc.clientMode, "size": len(allCategory)}, LogInfo, InfoCachedAllEntries)
}
case ContentTypeProduct:
allProduct, err := cc.cacheAllProduct(ctx, results)
if err != nil {
return errors.New("updateCacheForContentType failed for contentType product: " + err.Error())
}
tempCache.entryMaps.product = allProduct
cc.cacheMutex.genericEntriesGcLock.Lock()
for _, product := range allProduct {
tempCache.genericEntries[product.Sys.ID] = &GenericEntry{
Sys: product.Sys,
RawFields: product.RawFields,
CC: product.CC,
}
}
cc.cacheMutex.genericEntriesGcLock.Unlock()
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"contentType": "product", "method": "updateCacheForContentType", "clientMode": cc.clientMode, "size": len(allProduct)}, LogInfo, InfoCachedAllEntries)
}
case assetWorkerType:
allAssets, err := cc.getAllAssets(ctx, false)
if err != nil {
return errors.New("updateCacheForContentType failed for assets")
}
tempCache.assets = allAssets
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"contentType": "asset", "method": "updateCacheForContentType", "clientMode": cc.clientMode, "size": len(allAssets)}, LogInfo, InfoCachedAllAssets)
}
case tagWorkerType:
allTags, err := cc.getAllTags(ctx, false)
if err != nil {
return errors.New("updateCacheForContentType failed for tags")
}
tempCache.tags = allTags
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"contentType": "tag", "method": "updateCacheForContentType", "clientMode": cc.clientMode, "size": len(allTags)}, LogInfo, InfoCachedAllTags)
}
}
return nil
}
func updateCacheForContentTypeAndEntity(ctx context.Context, cc *ContentfulClient, contentType string, entityID string, entityPayload interface{}, entryDelete bool) error {
if ctx.Err() != nil {
return ctx.Err()
}
var entityEntry *contentful.Entry
var entityAsset *contentful.Asset
switch contentType {
case assetWorkerType:
if entryDelete {
err := cc.deleteAssetFromCache(entityID)
if err != nil {
return err
}
} else {
if entityPayload != nil {
entityAsset = entityPayload.(*contentful.Asset)
}
err := cc.cacheGcAssetByID(ctx, entityID, entityAsset)
if err != nil {
return err
}
}
if cc.logFn != nil && cc.logLevel <= LogInfo && entityPayload == nil {
cc.logFn(map[string]interface{}{"contentType": "Asset", "method": "updateCacheForContentTypeAndEntity", "clientMode": cc.clientMode, "entityID": entityID}, LogInfo, InfoUpdatedEntityCache)
}
case ContentTypeBrand:
if entityPayload != nil {
entityEntry = entityPayload.(*contentful.Entry)
}
err := cc.cacheBrandByID(ctx, entityID, entityEntry, entryDelete)
if err != nil {
return err
}
if cc.logFn != nil && cc.logLevel <= LogInfo && entityPayload == nil {
cc.logFn(map[string]interface{}{"contentType": "brand", "method": "updateCacheForContentTypeAndEntity", "clientMode": cc.clientMode, "entityID": entityID}, LogInfo, InfoUpdatedEntityCache)
}
case ContentTypeCategory:
if entityPayload != nil {
entityEntry = entityPayload.(*contentful.Entry)
}
err := cc.cacheCategoryByID(ctx, entityID, entityEntry, entryDelete)
if err != nil {
return err
}
if cc.logFn != nil && cc.logLevel <= LogInfo && entityPayload == nil {
cc.logFn(map[string]interface{}{"contentType": "category", "method": "updateCacheForContentTypeAndEntity", "clientMode": cc.clientMode, "entityID": entityID}, LogInfo, InfoUpdatedEntityCache)
}
case ContentTypeProduct:
if entityPayload != nil {
entityEntry = entityPayload.(*contentful.Entry)
}
err := cc.cacheProductByID(ctx, entityID, entityEntry, entryDelete)
if err != nil {
return err
}
if cc.logFn != nil && cc.logLevel <= LogInfo && entityPayload == nil {
cc.logFn(map[string]interface{}{"contentType": "product", "method": "updateCacheForContentTypeAndEntity", "clientMode": cc.clientMode, "entityID": entityID}, LogInfo, InfoUpdatedEntityCache)
}
}
allTags, err := cc.getAllTags(ctx, false)
if err != nil {
return fmt.Errorf("syncCache failed for tags: %s, clientMode: %s", err.Error(), cc.clientMode)
}
cc.cacheMutex.sharedDataGcLock.Lock()
cc.Cache.tags = allTags
cc.cacheMutex.sharedDataGcLock.Unlock()
return nil
}
func commonGetParents(ctx context.Context, cc *ContentfulClient, id string, contentTypes []string) (parents []EntryReference, err error) {
parents = []EntryReference{}
cc.cacheMutex.sharedDataGcLock.RLock()
cacheInit := cc.cacheInit
cc.cacheMutex.sharedDataGcLock.RUnlock()
if cacheInit {
cc.cacheMutex.parentMapGcLock.RLock()
defer cc.cacheMutex.parentMapGcLock.RUnlock()
if len(contentTypes) != 0 {
for _, parent := range cc.Cache.parentMap[id] {
for _, contentType := range contentTypes {
if parent.ContentType == contentType {
parents = append(parents, parent)
}
}
}
return parents, nil
}
return cc.Cache.parentMap[id], nil
}
col := cc.Client.Entries.List(ctx, cc.SpaceID)
col.Query.Equal("links_to_entry", id).Locale("*")
_, err = col.GetAll()
if err != nil {
return nil, errors.New("GetParents: " + err.Error())
}
for _, item := range col.Items {
var entry contentful.Entry
byteArray, err := json.Marshal(item)
if err != nil {
return nil, err
}
err = json.Unmarshal(byteArray, &entry)
if err != nil {
return nil, err
}
if len(contentTypes) == 1 && contentTypes[0] != entry.Sys.ContentType.Sys.ID {
continue
}
switch entry.Sys.ContentType.Sys.ID {
case ContentTypeBrand:
var parentVO CfBrand
byteArray, err := json.Marshal(item)
if err != nil {
return nil, errors.New("GetParents: " + err.Error())
}
err = json.NewDecoder(bytes.NewReader(byteArray)).Decode(&parentVO)
if err != nil {
return nil, errors.New("GetParents: " + err.Error())
}
parentVO.CC = cc
parents = append(
parents, EntryReference{
ContentType: entry.Sys.ContentType.Sys.ID,
ID: entry.Sys.ID,
VO: &parentVO,
CC: cc,
})
case ContentTypeCategory:
var parentVO CfCategory
byteArray, err := json.Marshal(item)
if err != nil {
return nil, errors.New("GetParents: " + err.Error())
}
err = json.NewDecoder(bytes.NewReader(byteArray)).Decode(&parentVO)
if err != nil {
return nil, errors.New("GetParents: " + err.Error())
}
parentVO.CC = cc
parents = append(
parents, EntryReference{
ContentType: entry.Sys.ContentType.Sys.ID,
ID: entry.Sys.ID,
VO: &parentVO,
CC: cc,
})
case ContentTypeProduct:
var parentVO CfProduct
byteArray, err := json.Marshal(item)
if err != nil {
return nil, errors.New("GetParents: " + err.Error())
}
err = json.NewDecoder(bytes.NewReader(byteArray)).Decode(&parentVO)
if err != nil {
return nil, errors.New("GetParents: " + err.Error())
}
parentVO.CC = cc
parents = append(
parents, EntryReference{
ContentType: entry.Sys.ContentType.Sys.ID,
ID: entry.Sys.ID,
VO: &parentVO,
CC: cc,
})
}
}
return parents, nil
}
// Unicode clean-up
func cleanUpStringField(field map[string]string) map[string]string {
cleanField := map[string]string{}
for locale, value := range field {
cleanField[locale] = stripInvisibleUnicodeChars(value)
}
return cleanField
}
func cleanUpStringSliceField(field map[string][]string) map[string][]string {
cleanField := map[string][]string{}
for locale, value := range field {
cleanLocalizedSliceElems := []string{}
for _, sliceElem := range value {
cleanLocalizedSliceElems = append(cleanLocalizedSliceElems, stripInvisibleUnicodeChars(sliceElem))
}
cleanField[locale] = cleanLocalizedSliceElems
}
return cleanField
}
func cleanUpRichTextField(field map[string]interface{}) map[string]interface{} {
cleanField := map[string]interface{}{}
for locale, value := range field {
node, err := objectToRichTextGenericNode(value)
if err != nil {
return field
}
cleanNode := cleanUpRichTextIterateNode(node)
cleanField[locale] = cleanNode
}
return cleanField
}
func cleanUpRichTextIterateNode(node *RichTextGenericNode) *RichTextGenericNode {
cleanNode := &RichTextGenericNode{
NodeType: node.NodeType,
Data: node.Data,
Value: stripInvisibleUnicodeChars(node.Value),
Marks: node.Marks,
}
for _, childNode := range node.Content {
cleanNode.Content = append(cleanNode.Content, cleanUpRichTextIterateNode(childNode))
}
return cleanNode
}
func isFieldRichText(field map[string]interface{}) bool {
for _, value := range field {
if value == nil {
continue
}
node, err := objectToRichTextGenericNode(value)
if err != nil {
return false
}
if node.NodeType == "document" && node.Content != nil {
return true
}
}
return false
}
func objectToRichTextGenericNode(value interface{}) (*RichTextGenericNode, error) {
node := &RichTextGenericNode{}
byt, err := json.Marshal(value)
if err != nil {
return nil, err
}
err = json.Unmarshal(byt, node)
if err != nil {
return nil, err
}
return node, nil
}
func stripInvisibleUnicodeChars(dirty string) string {
clean := strings.Map(func(r rune) rune {
if unicode.IsGraphic(r) || unicode.IsControl(r) {
return r
}
return -1
}, dirty)
return clean
}
func (rawFields RawFields) GetChildIDs() (childIDs []string) {
for fieldKey, localizedField := range rawFields {
if !strings.HasSuffix(fieldKey, "_") {
continue
}
byt, err := json.Marshal(localizedField)
if err != nil {
return nil
}
fieldMap := map[string][]*ContentTypeSys{}
err = json.Unmarshal(byt, &fieldMap)
if err != nil {
return nil
}
for _, fieldValue := range fieldMap {
if fieldValue != nil {
for _, childItem := range fieldValue {
childIDs = append(childIDs, childItem.Sys.ID)
}
}
}
}
return childIDs
}
func HasAncestor(ctx context.Context, contentType string, entry entryOrRef, visited map[string]bool) (*EntryReference, error) {
if visited == nil {
visited = make(map[string]bool)
}
parents, err := entry.GetParents(ctx)
if err != nil {
return nil, err
}
for _, parent := range parents {
if visited[parent.ID] {
continue
}
visited[parent.ID] = true
if parent.ContentType == contentType {
return &parent, nil
}
return HasAncestor(ctx, contentType, parent, visited)
}
return nil, nil
}