package com.celements.nextfreedoc; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.component.annotation.Component; import org.xwiki.component.annotation.Requirement; import org.xwiki.context.Execution; import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.SpaceReference; import org.xwiki.query.Query; import org.xwiki.query.QueryException; import org.xwiki.query.QueryManager; import com.celements.model.access.IModelAccessFacade; import com.celements.model.access.exception.DocumentAlreadyExistsException; import com.celements.model.access.exception.DocumentLoadException; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.XWikiDocument; @Singleton @Component public class NextFreeDocService implements INextFreeDocRole { private static Logger LOGGER = LoggerFactory.getLogger(NextFreeDocService.class); // TODO refactor to org.xwiki.cache.CacheManager private final Map<DocumentReference, Long> numCache = new HashMap<>(); @Requirement private QueryManager queryManager; @Requirement IModelAccessFacade modelAccess; @Requirement private Execution execution; private XWikiContext getContext() { return (XWikiContext) execution.getContext().getProperty("xwikicontext"); } @Override public DocumentReference getNextTitledPageDocRef(SpaceReference spaceRef, String title) { DocumentReference baseDocRef = new DocumentReference(title, spaceRef); return createDocRef(baseDocRef, getNextTitledPageNum(baseDocRef)); } @Override public DocumentReference getNextUntitledPageDocRef(SpaceReference spaceRef) { return getNextTitledPageDocRef(spaceRef, UNTITLED_NAME); } private synchronized long getNextTitledPageNum(DocumentReference baseDocRef) { long num = getHighestNum(baseDocRef); while (!isAvailableDocRef(createDocRef(baseDocRef, num))) { num += 1; } numCache.put(baseDocRef, num + 1); return num; } private boolean isAvailableDocRef(DocumentReference newDocRef) { try { XWikiDocument doc = modelAccess.createDocument(newDocRef); if (doc.getLock(getContext()) == null) { // TODO use (not yet existing) ModelLockService.aquireLock(doc) for cluster concurrency // safety doc.setLock(getContext().getUser(), getContext()); return true; } } catch (DocumentAlreadyExistsException exp) { LOGGER.trace("New docRef already exists '{}'", newDocRef, exp); } catch (XWikiException | DocumentLoadException exp) { LOGGER.error("Failed to check new docRef '{}'", newDocRef, exp); } return false; } private DocumentReference createDocRef(DocumentReference baseDocRef, long num) { // IMPORTANT do no use setName on DocumentReference // -> it does not exist on xwiki 4.5.4 DocumentReference ret = new DocumentReference(baseDocRef.getName() + num, baseDocRef.getLastSpaceReference()); return ret; } /** * NOTE: only use in synchronized context due to numCache * * @param baseDocRef * @return */ long getHighestNum(DocumentReference baseDocRef) { Long num = numCache.get(baseDocRef); try { int offset = 0, limit = 8; List<Object> results; while ((num == null) && ((results = getHighestNumQuery(baseDocRef, offset, limit).execute()).size() > 0)) { num = extractNumFromResults(baseDocRef.getName(), results); offset += results.size(); limit *= 2; } } catch (QueryException queryExc) { LOGGER.error("Error executing query '{}'", getHighestNumHQL(), queryExc); } if (num == null) { num = 1L; } LOGGER.debug("getHighestNum: for baseDocRef '{}' got '{}'", baseDocRef, num); return num; } private Query getHighestNumQuery(DocumentReference baseDocRef, int offset, int limit) throws QueryException { Query query = queryManager.createQuery(getHighestNumHQL(), Query.HQL); query.setOffset(offset); query.setLimit(limit); query.setWiki(baseDocRef.getWikiReference().getName()); query.bindValue("space", baseDocRef.getLastSpaceReference().getName()); query.bindValue("name", baseDocRef.getName() + "%"); return query; } String getHighestNumHQL() { return "SELECT doc.name FROM XWikiDocument doc WHERE doc.space=:space " + "AND doc.name LIKE :name ORDER BY LENGTH(doc.name) DESC, doc.name DESC"; } Long extractNumFromResults(String prefix, List<Object> results) { Long num = null; for (Object name : results) { try { if (name.toString().startsWith(prefix)) { String numStr = name.toString().substring(prefix.length()); num = Long.parseLong(numStr) + 1; break; } else { LOGGER.warn("extractNumFromResults: given name '{}' does not start with expected" + " prefix '{}'", name, prefix); } } catch (NumberFormatException nfExc) { LOGGER.debug("extractNumFromResults: unable to parse digit in '{}'", name); } } if (LOGGER.isTraceEnabled()) { LOGGER.trace("extractNumFromResults: got '{}' from docs: {}", num, results); } return num; } void injectQueryManager(QueryManager queryManager) { this.queryManager = queryManager; } /** * USE FOR TEST PURPOSES ONLY */ public void injectNum(SpaceReference spaceRef, String title, long num) { DocumentReference baseDocRef = new DocumentReference(title, spaceRef); numCache.put(baseDocRef, num); } }