const getContextURI = (es, c) => {
if (c && c.getResourceURI) {
return c.getResourceURI();
} else if (typeof c === 'string' && c !== '') {
if (c.indexOf('http') === 0) {
return c;
} else {
return es.getContextById(c).getResourceURI();
}
}
};
const contextEquals = (es, c1, c2) => getContextURI(es, c1) === getContextURI(es, c2);
const promiseInScope = (es, promise, c2) => getContextURI(es, promise.__context) === getContextURI(es, c2);
const markPromise = (es, promise, context) => {
const curi = getContextURI(es, context);
if (curi) {
promise.__context = curi;
}
};
/**
* EntryStoreUtil provides utility functionality for working with entries.
* @exports store/EntryStoreUtil
*/
export default class EntryStoreUtil {
/**
* @param {EntryStore} entrystore
*/
constructor(entrystore) {
this._entrystore = entrystore;
this._preloadIdx = new Map();
this._publicRead = false;
this._debounceQueue = [];
this._debounceContext;
this._debounceAsyncCallType;
}
/**
* When loading entries via solr queries restrict to those marked as public.
* Corresponds to calling publicRead on all solr queries used internally in these help functions.
* @param {boolean} publicRead
*/
loadOnlyPublicEntries(publicRead) {
this._publicRead = publicRead;
}
/**
* @returns {EntryStore}
*/
getEntryStore() {
return this._entrystore;
}
/**
* Preload entries of a specific type.
* Not strictly needed, used for optimization reasons.
* Up to a maximum of 100 entries are preloaded.
*
* @param {string} ofType
* @param {Context} context if provided limits the preload to a specific context.
* @returns {Entry}
*/
preloadEntries(ofType, context) {
let preloadForType = this._preloadIdx.get(ofType);
let promise;
if (preloadForType) {
if (context) {
promise = preloadForType[context.getEntryURI()];
if (promise) {
return promise;
}
} else if (preloadForType.noContext) {
return preloadForType.noContext;
}
} else {
preloadForType = {};
this._preloadIdx.set(ofType, preloadForType);
}
const searchObj = this._entrystore.newSolrQuery().resourceType(ofType).limit(100);
if (this._publicRead) {
searchObj.publicRead(true);
}
if (context) {
searchObj.context(context);
}
const list = searchObj.list();
promise = list.getEntries(0);
if (context) {
preloadForType[context.getEntryURI()] = promise;
} else {
preloadForType.noContext = promise;
}
return promise;
}
clearPreloadEntriesDuplicateCheck(ofType, inContext) {
if (ofType) {
const preloadForType = this._preloadIdx.get(ofType);
if (preloadForType && inContext) {
delete preloadForType[inContext.getEntryURI()];
} else {
this._preloadIdx.delete(ofType);
}
} else {
this._preloadIdx = new Map();
}
}
/**
* Retrieves an entry for a resource URI, note that if there are several entries that all
* have the same resource URI it is unclear which of these entries that are returned.
* Hence, only use this function if you expect there to be a single entry per resource URI.
*
* @param {string} resourceURI is the URI for the resource.
* @param {Context=} context only look for entries in this context, may be left out.
* @param {string} asyncCallType the callType used when making the search.
* @returns {Promise.<Entry>}
* @async
* @throws Error
*/
async getEntryByResourceURI(resourceURI, context, asyncCallType) {
return this.loadEntriesByResourceURIs([resourceURI], context, false, asyncCallType)
.then(arr => arr[0]);
}
async _joinedDebouncedRequest() {
const debounceList = this._debounceQueue;
const entriesPromise = this.loadEntriesByResourceURIs(debounceList.map(args => args[0]),
this._debounceContext, true, this._debounceAsyncCallType);
this._debounceQueue = [];
this._debounceContext = undefined;
this._debounceAsyncCallType = undefined;
const entries = await entriesPromise;
for (let i = 0; i < debounceList.length; i++) {
if (entries[i]) {
debounceList[i][1](entries[i]);
} else {
debounceList[i][2](`Could not find entry with URI ${debounceList[i][0]}`);
}
}
}
/**
* Retrieves an entry for a resource URI in a similar manner as the method getEntryByResourceURI,
* but with the distinction that it delays the request for a few milliseconds to allow collecting
* multiple requests together into a single request.
*
* @param {string} resourceURI is the URI for the resource.
* @param {Context=} context only look for entries in this context, may be left out.
* @param {string} asyncCallType the callType used when making the search.
* @returns {Promise.<Entry>}
* @async
* @throws Error
*/
async getEntryByResourceURIDebounce(resourceURI, context, asyncCallType) {
// Check first if the entry is already in the cache, then return it directly.
const cache = this._entrystore.getCache();
const entriesSet = cache.getByResourceURI(resourceURI);
if (entriesSet.size > 0) {
if (context) {
for (const entry of entriesSet) { // eslint-disable-line
if (entry.getContext().getId() === context.getId()) {
return Promise.resolve(entry);
}
}
} else {
return Promise.resolve(entriesSet.values().next().value);
}
}
// If the request is different in the sense that it requires a different context or asyncCallType
// then we have to request entries in the debounce queue first and then add to the queue again with the new context or callType.
if (this._debounceQueue.length > 0 && (this._debounceContext !== context || this._debounceAsyncCallType !== asyncCallType)) {
clearTimeout(this._debounceTimeout);
this._joinedDebouncedRequest();
}
return new Promise((resolve, reject) => {
// Nothing in the debounce queue, start the timeout
if (this._debounceQueue.length === 0) {
this._debounceTimeout = setTimeout(this._joinedDebouncedRequest.bind(this), 20);
}
this._debounceQueue.push([resourceURI, resolve, reject]);
this._debounceContext = context;
this._debounceAsyncCallType = asyncCallType;
});
}
/**
* @param {string} resourceURI is the URI for the resource.
* @returns {Entry}
*/
getEntryListByResourceURI(resourceURI) {
const query = this._entrystore.newSolrQuery().resource(resourceURI);
if (this._publicRead) {
query.publicRead(true);
}
return query.list();
}
/**
* Attempting to find a unique entry for a specific type,
* if multiple entries exists with the same type the returned promise fails.
* You may restrict to a specific context.
*
* @param {string} typeURI is the rdf:type URI for the entry to match.
* @param {Context} context restrict to finding the entry in this context
* @param {string} asyncCallType the callType used when making the search.
* @returns {Promise.<Entry>}
* @async
* @throws Error
*/
async getEntryByType(typeURI, context, asyncCallType) {
const query = this._entrystore.newSolrQuery().rdfType(typeURI).limit(2);
if (this._publicRead) {
query.publicRead(true);
}
if (context) {
query.context(context);
}
const entryArr = await query.list(asyncCallType).getEntries(0);
if (entryArr.length === 1) {
return entryArr[0];
}
throw new Error('Wrong number of entries in context / repository');
}
/**
* Attempting to find one entry for a specific graph type,
* if multiple entries exists with the same type the returned promise fails.
* You may restrict to a specific context.
*
* @param {string} graphType is the graph type for the entry to match, e.g. use
* {@see types#GT_USER}.
* @param {Context} context restrict to finding the entry in this context
* @param {string} asyncCallType the callType used when making the search.
* @returns {Promise.<Entry>}
* @async
* @throws Error
*/
async getEntryByGraphType(graphType, context, asyncCallType) {
const query = this._entrystore.newSolrQuery().graphType(graphType).limit(2);
if (this._publicRead) {
query.publicRead(true);
}
if (context) {
query.context(context);
}
const entryArr = await query.list(asyncCallType).getEntries(0);
if (entryArr.length > 0) {
return entryArr[0];
}
throw new Error(`No entries in ${context ? 'context' : 'repository'} context with graphType ${graphType}`);
}
/**
* Removes all entries matched by a search in a serial manner,
* also empties the cache from loaded entries so it should not overflow
* if the searchlist is big.
*
* The removal is accomplished by first iterating through the searchlist and collecting
* uris to all entries that should be removed. After that the entries are removed.
*
* @param {SearchList} list
* @returns {Promise<String[]>} array of uris that where deleted
*/
async removeAll(list) {
const uris = [];
const deleted = [];
const es = this._entrystore;
const cache = es.getCache();
const rest = es.getREST();
const deleteNext = async () => {
if (uris.length > 0) {
const uri = uris.pop();
try {
await rest.del(uri);
deleted.push(uri);
} catch (err) {
console.log(`Could not remove entry with uri: ${uri} continuing anyway.`);
}
await deleteNext();
}
return undefined;
};
const result = await list.forEach((entry) => {
uris.push(entry.getURI());
cache.unCache(entry);
});
await deleteNext(result);
return deleted;
}
/**
* Loads entries by first checking if they are in the cache, second if there are ongoing loading attempts that
* can be waited on and lastly loads them itself by via a solr query. Note, if too many entries are asked for at
* once the solr query will be divided into smaller chunks.
*
* @param {Array<String>} resourceURIs array of resourceURIs to load.
* @param {Context=} context only look for entries in this context, may be left out.
* @param {boolean} acceptMissing if true then the array returned may contain holes
* @param {string} asyncCallType the callType used when making the search.
* @returns {Promise<Entry[]>}
*/
async loadEntriesByResourceURIs(resourceURIs, context, acceptMissing = false, asyncCallType) {
const es = this._entrystore;
const cache = es.getCache();
const id2Entry = {};
const previouslyLoadingPromises = [];
const toLoadSet = new Set();
resourceURIs.forEach((uri) => {
let entries = Array.from(cache.getByResourceURI(uri).values());
if (context) {
entries = entries.filter(e => e.getContext().getResourceURI() === getContextURI(es, context));
}
if (entries.length > 0) {
id2Entry[uri] = entries[0];
} else {
const loadpromise = cache.getPromise(uri);
if (loadpromise && promiseInScope(es, loadpromise, context)) {
previouslyLoadingPromises.push(loadpromise.then((entry) => {
id2Entry[uri] = entry;
}, (e) => {
if (!acceptMissing) {
throw e;
}
}));
} else {
toLoadSet.add(uri);
}
}
});
const chunked = [];
const chunkLimit = 20;
const toLoad = Array.from(toLoadSet);
for (let i = 0; i < toLoad.length; i += chunkLimit) {
chunked.push(toLoad.slice(i, i + chunkLimit));
}
const chunkLoadingPromises = chunked.map((chunk) => {
const uri2resolve = {};
const uri2reject = {};
chunk.forEach((ruri) => {
const p = new Promise((resolve, reject) => {
uri2resolve[ruri] = resolve;
uri2reject[ruri] = reject;
});
markPromise(es, p, context);
cache.addPromise(ruri, p);
});
const loadEntries = new Set(chunk);
const query = es.newSolrQuery();
if (this._publicRead) {
query.publicRead(true);
}
return query.resource(chunk).context(context).list(asyncCallType).forEach((entry) => {
const ruri = entry.getResourceURI();
if (loadEntries.has(ruri)) {
loadEntries.delete(ruri);
uri2resolve[ruri](entry);
id2Entry[ruri] = entry;
cache.removePromise(ruri);
}
return loadEntries.size !== 0;
}).then(() => {
if (loadEntries.size > 0) {
loadEntries.forEach((ruri) => {
uri2reject[ruri](new Error(`No resource found for ${ruri}`));
cache.removePromise(ruri);
});
if (!acceptMissing) {
throw new Error(`The following resources could not be found ${Array.from(loadEntries).join(', ')}`);
}
}
});
});
return Promise.all(previouslyLoadingPromises.concat(chunkLoadingPromises))
.then(() => resourceURIs.map(ruri => id2Entry[ruri] || null));
}
}