/* * Copyright (C) 2005-2012 BetaCONCEPT Limited * * This file is part of Astroboa. * * Astroboa is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Astroboa is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Astroboa. If not, see <http://www.gnu.org/licenses/>. */ package org.betaconceptframework.astroboa.engine.jcr.dao; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.List; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Value; import javax.jcr.ValueFormatException; import javax.jcr.lock.LockException; import javax.jcr.nodetype.ConstraintViolationException; import javax.jcr.version.VersionException; import javax.jcr.version.VersionHistory; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.time.DurationFormatUtils; import org.betaconceptframework.astroboa.api.model.BinaryChannel; import org.betaconceptframework.astroboa.api.model.CalendarProperty; import org.betaconceptframework.astroboa.api.model.CmsProperty; import org.betaconceptframework.astroboa.api.model.CmsRepositoryEntity; import org.betaconceptframework.astroboa.api.model.ContentObject; import org.betaconceptframework.astroboa.api.model.ContentObjectFolder; import org.betaconceptframework.astroboa.api.model.StringProperty; import org.betaconceptframework.astroboa.api.model.definition.CmsPropertyDefinition; import org.betaconceptframework.astroboa.api.model.definition.ContentObjectTypeDefinition; import org.betaconceptframework.astroboa.api.model.exception.CmsConcurrentModificationException; import org.betaconceptframework.astroboa.api.model.exception.CmsException; import org.betaconceptframework.astroboa.api.model.io.FetchLevel; import org.betaconceptframework.astroboa.api.model.io.ImportConfiguration; import org.betaconceptframework.astroboa.api.model.io.ImportConfiguration.PersistMode; import org.betaconceptframework.astroboa.api.model.io.ResourceRepresentationType; import org.betaconceptframework.astroboa.api.model.io.SerializationConfiguration; import org.betaconceptframework.astroboa.api.model.query.CacheRegion; import org.betaconceptframework.astroboa.api.model.query.CmsOutcome; import org.betaconceptframework.astroboa.api.model.query.criteria.ContentObjectCriteria; import org.betaconceptframework.astroboa.api.model.query.render.RenderProperties; import org.betaconceptframework.astroboa.context.AstroboaClientContextHolder; import org.betaconceptframework.astroboa.engine.cache.regions.JcrQueryCacheRegion; import org.betaconceptframework.astroboa.engine.jcr.io.SerializationBean.CmsEntityType; import org.betaconceptframework.astroboa.engine.jcr.query.CalendarInfo; import org.betaconceptframework.astroboa.engine.jcr.query.CmsQueryHandler; import org.betaconceptframework.astroboa.engine.jcr.query.CmsQueryResult; import org.betaconceptframework.astroboa.engine.jcr.renderer.BinaryChannelRenderer; import org.betaconceptframework.astroboa.engine.jcr.renderer.ContentObjectFolderRenderer; import org.betaconceptframework.astroboa.engine.jcr.renderer.ContentObjectRenderer; import org.betaconceptframework.astroboa.engine.jcr.util.CmsRepositoryEntityUtils; import org.betaconceptframework.astroboa.engine.jcr.util.Context; import org.betaconceptframework.astroboa.engine.jcr.util.JcrNodeUtils; import org.betaconceptframework.astroboa.engine.jcr.util.JcrValueUtils; import org.betaconceptframework.astroboa.engine.jcr.util.VersionUtils; import org.betaconceptframework.astroboa.engine.model.lazy.local.LazyComplexCmsPropertyLoader; import org.betaconceptframework.astroboa.model.factory.CmsCriteriaFactory; import org.betaconceptframework.astroboa.model.impl.ComplexCmsPropertyImpl; import org.betaconceptframework.astroboa.model.impl.ContentObjectImpl; import org.betaconceptframework.astroboa.model.impl.SaveMode; import org.betaconceptframework.astroboa.model.impl.item.CmsBuiltInItem; import org.betaconceptframework.astroboa.model.impl.item.ContentObjectProfileItem; import org.betaconceptframework.astroboa.model.impl.item.DefinitionReservedName; import org.betaconceptframework.astroboa.model.impl.item.JcrBuiltInItem; import org.betaconceptframework.astroboa.model.impl.query.CmsOutcomeImpl; import org.betaconceptframework.astroboa.model.impl.query.render.RenderPropertiesImpl; import org.betaconceptframework.astroboa.util.CmsConstants; import org.betaconceptframework.astroboa.util.DateUtils; import org.betaconceptframework.astroboa.util.PropertyPath; import org.betaconceptframework.astroboa.util.TreeDepth; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** * * @author Gregory Chomatas (gchomatas@betaconcept.com) * @author Savvas Triantafyllou (striantafyllou@betaconcept.com) * */ public class ContentDao extends JcrDaoSupport{ private final Logger logger = LoggerFactory.getLogger(ContentDao.class); @Autowired private CmsRepositoryEntityUtils cmsRepositoryEntityUtils; @Autowired private ContentObjectDao contentObjectDao; @Autowired private JcrQueryCacheRegion jcrQueryCacheRegion; @Autowired private ContentDefinitionDao contentDefinitionDao; @Autowired private BinaryChannelRenderer binaryChannelRenderer; @Autowired private ContentObjectFolderRenderer contentObjectFolderRenderer; @Autowired private ContentObjectRenderer contentObjectRenderer; @Autowired private VersionUtils versionUtils; @Autowired private LazyComplexCmsPropertyLoader lazyComplexCmsPropertyLoader; @Autowired private CmsQueryHandler cmsQueryHandler; @Autowired private SerializationDao serializationDao; @Autowired private ImportDao importDao; /** * * @param contentObject * @param version * @param lockToken * @param updateLastModificationTime Flag to indicate that profile.modified should be updated * @return */ public ContentObject saveContentObject(Object contentSource, boolean version, String lockToken, boolean updateLastModificationTime, Context context) { long start = System.currentTimeMillis(); if (contentSource == null){ throw new CmsException("Cannot save null content object"); } if (contentSource instanceof String){ logger.debug(" Starting saving content object. First import will take place"); //Use importer to unmarshal String to ContentObject //and to save it as well. //What is happened is that importDao will create a ContentObject //and will pass it to ContentServiceImpl to save it. //It will end up in this method again as a ContentObject //if it passes the check of SecureContentObjectSaveAspect ImportConfiguration configuration = ImportConfiguration.object() .persist(PersistMode.PERSIST_ENTITY_TREE) .version(version) .updateLastModificationTime(updateLastModificationTime) .build(); return importDao.importContentObject((String)contentSource, configuration); } if (! (contentSource instanceof ContentObject)){ throw new CmsException("Expecting either String or ContentObject and not "+contentSource.getClass().getName()); } ContentObject contentObject = (ContentObject) contentSource; logger.debug(" Starting saving content object {}", contentObject.getSystemName()); SaveMode saveMode = null; Session session = null; boolean disposeContextWhenSaveIsFinished = false; try { saveMode = cmsRepositoryEntityUtils.determineSaveMode(contentObject); //Populate content node with appropriate information if (context == null){ context = new Context(cmsRepositoryEntityUtils, cmsQueryHandler, getSession()); disposeContextWhenSaveIsFinished = true; } session = context.getSession(); if (StringUtils.isNotBlank(lockToken)){ session.getWorkspace().getLockManager().addLockToken(lockToken); } primaryCheck(contentObject); Node contentObjectNode = null; Calendar modifiedDateBeforeSave = null; switch (saveMode) { case INSERT: contentObjectNode = createNewContentObjectNode(contentObject, session, false); break; case UPDATE: contentObjectNode = cmsRepositoryEntityUtils.retrieveUniqueNodeForContentObject(session, contentObject.getId()); if (contentObjectNode == null){ //User has provided an id for content object. Create a new one with the specified id contentObjectNode = createNewContentObjectNode(contentObject, session, true); } else{ //If node is locked and no valid lock token is provided then an exception is thrown session.getWorkspace().getVersionManager().checkout(contentObjectNode.getPath()); modifiedDateBeforeSave = retrieveProfileModifiedFromContentObjectNode(contentObjectNode); checkModificationDateBeforeSave(contentObject, updateLastModificationTime, modifiedDateBeforeSave); } break; default: break; } contentObjectDao.populateContentObjectNode(session, contentObject, contentObjectNode, saveMode, context); //Check if someone has already been saved by another user checkModificationDateAfterSave(contentObject, updateLastModificationTime, contentObjectNode, modifiedDateBeforeSave); session.save(); if (version){ session.getWorkspace().getVersionManager().checkin(contentObjectNode.getPath()); } //This must run after save was successful because these changes cannot not be rolled back ((ComplexCmsPropertyImpl)contentObject.getComplexCmsRootProperty()).clearCmsPropertyNameWhichHaveBeenLoadedAndRemovedList(); return contentObject; } catch(CmsException e) { throw e; } catch(Throwable e){ throw new CmsException(e); } finally{ if (disposeContextWhenSaveIsFinished){ if (context != null){ context.dispose(); context = null; } if (StringUtils.isNotBlank(lockToken)){ try { session.getWorkspace().getLockManager().removeLockToken(lockToken); } catch (RepositoryException e) { logger.error("Lock token "+lockToken+" could not be removed", e); } } } logger.debug(" Saved ContentObject {} in {}", contentObject.getSystemName(), DurationFormatUtils.formatDurationHMS(System.currentTimeMillis() - start)); } } private void checkModificationDateAfterSave(ContentObject contentObject, boolean updateLastModificationTime, Node contentObjectNode, Calendar modifiedDateBeforeSave) throws RepositoryException, ValueFormatException, PathNotFoundException, VersionException, LockException, ConstraintViolationException { if (modifiedDateBeforeSave != null){ //Retrieve modified date now Calendar modifiedDateAfterSave = retrieveProfileModifiedFromContentObjectNode(contentObjectNode); if (modifiedDateAfterSave != null && modifiedDateAfterSave.getTimeInMillis() != modifiedDateBeforeSave.getTimeInMillis()){ String pattern = "dd/MM/yyyy HH:mm:ss.SSSZ"; throw new CmsConcurrentModificationException("Content Object "+ contentObject.getId() +"/" +contentObject.getSystemName()+ " has been concurrently modified by another user or current user has tried to " + " set a value for profile.modified property. \n" + " profile.modified BEFORE save : "+ DateUtils.format(modifiedDateBeforeSave, pattern)+ "\n" + " profile.modified AFTER save : "+ DateUtils.format(modifiedDateAfterSave, pattern) ); } else{ //Profile.modified will be updated if updateLastModificationTime is set to true //or profile.modified has no values if (updateLastModificationTime || modifiedDateAfterSave == null){ //Update modification date Calendar modifiedDate = Calendar.getInstance(); //Update jcr node contentObjectNode.getProperty("profile/modified").setValue(modifiedDate); //Update ContentObject if (contentObject.getComplexCmsRootProperty().isChildPropertyLoaded("profile.modified")){ ((CalendarProperty)contentObject.getCmsProperty("profile.modified")).setSimpleTypeValue(modifiedDate); } } } } } private Calendar checkModificationDateBeforeSave(ContentObject contentObject, boolean updateLastModificationTime, Calendar modifiedDateBeforeSave) { if (modifiedDateBeforeSave != null){ //Make a check if provided ContentObject has a different modified date if (contentObject.getComplexCmsRootProperty() != null && contentObject.getComplexCmsRootProperty().isChildPropertyLoaded("profile.modified")){ CalendarProperty modifiedDateProvidedByUserProperty = (CalendarProperty) contentObject.getCmsProperty("profile.modified"); boolean throwException = false; if (modifiedDateProvidedByUserProperty.hasValues()){ Calendar modifiedDateProvidedByUser = modifiedDateProvidedByUserProperty.getSimpleTypeValue(); if (modifiedDateProvidedByUser == null || modifiedDateProvidedByUser.getTimeInMillis() != modifiedDateBeforeSave.getTimeInMillis()){ throwException = true; } else{ return modifiedDateBeforeSave; } } if (throwException){ String pattern = "dd/MM/yyyy HH:mm:ss.SSSZ"; throw new CmsConcurrentModificationException("Content Object "+ contentObject.getId() +"/" +contentObject.getSystemName()+ " has been concurrently modified by another user or current user has tried to " + " set a value for profile.modified property. \n" + " profile.modified BEFORE save : \t"+ DateUtils.format(modifiedDateBeforeSave, pattern)+ "(full info "+modifiedDateBeforeSave.toString()+")\n" + " profile.modified provided by user save : "+ (modifiedDateProvidedByUserProperty.hasNoValues() ? " No date ": DateUtils.format(modifiedDateProvidedByUserProperty.getSimpleTypeValue(), pattern)) + "(full info "+modifiedDateProvidedByUserProperty.getSimpleTypeValue()+")" ); } } } return null; } private Calendar retrieveProfileModifiedFromContentObjectNode(Node contentObjectNode) throws RepositoryException, ValueFormatException, PathNotFoundException { if (! contentObjectNode.hasProperty("profile/modified")){ return null; } else{ return contentObjectNode.getProperty("profile/modified").getDate(); } } private void primaryCheck(ContentObject contentObject) throws Exception { final String type = contentObject.getContentObjectType(); //Check if this type is defined if (StringUtils.isBlank(type)) throw new CmsException("Invalid type "+ type); if (!contentDefinitionDao.hasContentObjectTypeDefinition(type)) throw new CmsException("Unregistered content object type "+ type+ " in repository "+AstroboaClientContextHolder.getActiveRepositoryId()); } private Node createNewContentObjectNode(ContentObject contentObject, Session session, boolean useProvidedId ) throws Exception { String type = contentObject.getContentObjectType(); // Get Type folder node. If it does not exist, it will be Created Node contentTypeFolderNode = JcrNodeUtils.retrieveOrCreateContentTypeFolderNode(session, type); CalendarInfo calendarInfo = new CalendarInfo(Calendar.getInstance()); //Profile will provide year/month/day information String profileCreatedPropertyPath = ContentObjectProfileItem.Created.getItemForQuery().getLocalPart(); //Check that property created is defined for type ContentObjectTypeDefinition typeDefinition = contentDefinitionDao.getContentObjectTypeDefinition(type); if (typeDefinition.hasCmsPropertyDefinition(profileCreatedPropertyPath)){ CalendarProperty createdProperty = (CalendarProperty) contentObject.getCmsProperty(profileCreatedPropertyPath); if (createdProperty == null){ //Should never happen throw new CmsException("Content type "+ typeDefinition.getName()+ " defines built in property profile.created but could not create property instance"); } Calendar created = (createdProperty.hasNoValues())? null : (Calendar) createdProperty.getSimpleTypeValue(); if (created != null){ //In case create date has unset YEAR, MONTH, OR DAY //Calendar Info will set default values calendarInfo = new CalendarInfo(created); } //Update Created date createdProperty.setSimpleTypeValue(calendarInfo.getCalendar()); } else{ //Should never happen throw new CmsException("Content type "+ typeDefinition.getName()+ " does not define built in property profile.created"); } //If minute folder does not exist, it will be Created Node contentObjectParentNode = JcrNodeUtils.createContentObjectParentNode(contentTypeFolderNode, calendarInfo ); //Add new node. //Node's name will be the same with typeName Node contentObjectNode = JcrNodeUtils.addContentObjectNode(contentObjectParentNode, type); //Create or use provided astroboa identifier cmsRepositoryEntityUtils.createCmsIdentifier(contentObjectNode, contentObject, useProvidedId); //Add modified date String profileModifiedPropertyPath = ContentObjectProfileItem.Modified.getItemForQuery().getLocalPart(); if (typeDefinition.hasCmsPropertyDefinition(profileModifiedPropertyPath)) { CalendarProperty modifiedProperty = (CalendarProperty) contentObject.getCmsProperty(profileModifiedPropertyPath); if (modifiedProperty.hasNoValues()){ modifiedProperty.setSimpleTypeValue(calendarInfo.getCalendar()); } } else { //Should never happen throw new CmsException("Content type "+ typeDefinition.getName()+ " does not define built in property profile.modified"); } return contentObjectNode; } public BinaryChannel getBinaryChannelById(String binaryChannelId) { Session session = null; if (StringUtils.isBlank(binaryChannelId)) throw new CmsException("Blank binary channel id"); try { session = getSession(); Node binaryChannelNode = cmsRepositoryEntityUtils.retrieveUniqueNodeForBinaryChannel(session, binaryChannelId); return binaryChannelRenderer.render(binaryChannelNode, true); }catch(RepositoryException ex) { throw new CmsException(ex); } catch (Exception e) { throw new CmsException(e); } } public List<ContentObjectFolder> getRootContentObjectFolders(String locale){ return getRootContentObjectFolders(TreeDepth.ZERO.asInt(), locale); } public List<ContentObjectFolder> getRootContentObjectFolders(int depth, String locale) { Session session = null; try { session = getSession(); Node contentObjectRootNode = JcrNodeUtils.getContentObjectRootNode(session); NodeIterator contentObjectRootFolderNodes = contentObjectRootNode.getNodes(); List<ContentObjectFolder> outcome = new ArrayList<ContentObjectFolder>(); while (contentObjectRootFolderNodes.hasNext()) outcome.add(contentObjectFolderRenderer.render(session, contentObjectRootFolderNodes.nextNode(), depth, false, locale)); return outcome; }catch(RepositoryException ex) { throw new CmsException(ex); } catch (Exception e) { throw new CmsException(e); } } public ContentObjectFolder getContentObjectFolderTree(String parentFolderId, int depth, boolean renderContentObjectIds, String locale) { Session session = null; try { session = getSession(); Node contentObjectFolderNode = JcrNodeUtils.getNodeByNativeRepositoryIdentifier(session, parentFolderId); return contentObjectFolderRenderer.render(session, contentObjectFolderNode, depth, renderContentObjectIds, locale); }catch(RepositoryException ex) { throw new CmsException(ex); } catch (Exception e) { throw new CmsException(e); } } public boolean deleteContentObject(String objectIdOrSystemName) { Session session = null; Context context = null; try { session = getSession(); //Retrieve content object node Node contentObjectNode = getContentObjectNodeByIdOrSystemName(objectIdOrSystemName); if (contentObjectNode != null){ context = new Context(cmsRepositoryEntityUtils, cmsQueryHandler, session); contentObjectDao.removeContentObjectNode(contentObjectNode, true, session, context); session.save(); return true; } logger.info("Object [] does not exist and therefore cannot be deleted", objectIdOrSystemName); return false; //Clear cache //jcrQueryCacheRegion.removeRegion(); }catch(RepositoryException ex) { throw new CmsException(ex); } catch (Exception e) { throw new CmsException(e); } finally{ if (context != null){ context.dispose(); context = null; } } } public ContentObject getContentObjectByVersionName(String contentObjectId, String versionName, String locale, CacheRegion cacheRegion) { Session session = null; try { session = getSession(); //Retrieve content object version history node VersionHistory contentObjectVersionHistory = versionUtils.getVersionHistoryForNode(session, contentObjectId); if (contentObjectVersionHistory ==null) { return null; } RenderProperties renderProperties = new RenderPropertiesImpl(); //renderProperties.renderValuesForLocale(locale); renderProperties.renderVersionForContentObject(versionName); return contentObjectRenderer.render(session, contentObjectVersionHistory, renderProperties, new HashMap<String, ContentObjectTypeDefinition>(), new HashMap<String, CmsRepositoryEntity>()); }catch(RepositoryException ex) { throw new CmsException(ex); } catch (Exception e) { throw new CmsException(e); } } public <T> T searchContentObjects(ContentObjectCriteria contentObjectCriteria, ResourceRepresentationType<T> contentObjectOutput){ T queryResult = null; boolean queryReturnedAtLeastOneResult = false; ByteArrayOutputStream os = null; try { //Check if criteria is provided if (contentObjectCriteria == null){ return generateEmptyOutcome(contentObjectOutput); } //Initialize null parameters (if any) if (contentObjectOutput == null){ contentObjectOutput = (ResourceRepresentationType<T>) ResourceRepresentationType.CONTENT_OBJECT_LIST; } //Check cache if (contentObjectCriteria.isCacheable()){ queryResult = (T)jcrQueryCacheRegion.getJcrQueryResults(contentObjectCriteria, contentObjectOutput.getTypeAsString()); if (queryResult != null){ return queryResult; } } //User requested Objects as return type if (ResourceRepresentationType.CONTENT_OBJECT_INSTANCE.equals(contentObjectOutput)|| ResourceRepresentationType.CONTENT_OBJECT_LIST.equals(contentObjectOutput)){ CmsOutcome<ContentObject> outcome = contentObjectDao.searchContentObjects(contentObjectCriteria, getSession()); //User requested one ContentObject. Throw an exception if more than //one returned if (ResourceRepresentationType.CONTENT_OBJECT_INSTANCE.equals(contentObjectOutput)){ queryResult = (T) returnSingleContentObjectFromOutcome(contentObjectCriteria, outcome); queryReturnedAtLeastOneResult = queryResult != null; } else{ //Return type is CmsOutcome. queryResult = (T) outcome; queryReturnedAtLeastOneResult = outcome.getCount() > 0; } } else if (ResourceRepresentationType.XML.equals(contentObjectOutput)|| ResourceRepresentationType.JSON.equals(contentObjectOutput)){ //User requested output to be XML or JSON os = new ByteArrayOutputStream(); SerializationConfiguration serializationConfiguration = SerializationConfiguration.object() .prettyPrint(contentObjectCriteria.getRenderProperties().isPrettyPrintEnabled()) .representationType(contentObjectOutput) .serializeBinaryContent(false) .build(); long numberOfResutls = serializationDao.serializeSearchResults(getSession(), contentObjectCriteria, os, FetchLevel.ENTITY, serializationConfiguration); queryReturnedAtLeastOneResult = numberOfResutls > 0; queryResult = (T) new String(os.toByteArray(), "UTF-8"); } else{ throw new CmsException("Invalid resource representation type for content object search results "+contentObjectOutput); } if (contentObjectCriteria.isCacheable()){ String xpathQuery = contentObjectCriteria.getXPathQuery(); if (!StringUtils.isBlank(xpathQuery) && queryReturnedAtLeastOneResult){ jcrQueryCacheRegion.cacheJcrQueryResults(contentObjectCriteria, queryResult, contentObjectCriteria.getRenderProperties(), contentObjectOutput.getTypeAsString()); } } return queryResult; } catch(RepositoryException ex){ throw new CmsException(ex); } catch (Exception e) { throw new CmsException(e); } finally{ IOUtils.closeQuietly(os); } } private ContentObject returnSingleContentObjectFromOutcome(ContentObjectCriteria contentObjectCriteria, CmsOutcome<ContentObject> outcome) { //User set limit to 1. Return content object found //a result is returned if (outcome.getLimit() == 1){ if (CollectionUtils.isNotEmpty(outcome.getResults())){ return outcome.getResults().get(0); } else { return null; } } else{ //User specified limit different than 1 (either no limit or greater than 1) if (outcome.getCount() > 1){ throw new CmsException(outcome.getCount() +" content objects matched criteria, user has specified limit "+ contentObjectCriteria.getLimit() + " but she also requested return type to be a single ContentObject."); } else{ if (CollectionUtils.isNotEmpty(outcome.getResults())){ return outcome.getResults().get(0); } else { return null; } } } } private <T> T generateEmptyOutcome( ResourceRepresentationType<T> contentObjectOutput) { if (contentObjectOutput != null && contentObjectOutput.equals(ResourceRepresentationType.CONTENT_OBJECT_LIST)){ return (T) new CmsOutcomeImpl<T>(0, 0, 0); } else{ return null; } } public String lockContentObject(String contentObjectId) { Session session = null; try { session = getSession(); Node contentObjectNode = cmsRepositoryEntityUtils.retrieveUniqueNodeForContentObject(session, contentObjectId); if (contentObjectNode == null){ return null; } //Deep lock and open-scoped if (!contentObjectNode.isLocked()) session.getWorkspace().getLockManager().lock(contentObjectNode.getPath(), true, false, Long.MAX_VALUE, ""); String lockToken = session.getWorkspace().getLockManager().getLock(contentObjectNode.getPath()).getLockToken(); if (lockToken != null) { session.getWorkspace().getVersionManager().checkout(contentObjectNode.getPath()); contentObjectNode.setProperty(DefinitionReservedName.Locktoken.getJcrName(), lockToken); } else { //Lock Operation may have failed. Try get locktoken from contentObjectNode if (contentObjectNode.hasProperty(DefinitionReservedName.Locktoken.getJcrName())) lockToken = contentObjectNode.getProperty(DefinitionReservedName.Locktoken.getJcrName()).getString(); } session.save(); return lockToken; }catch(RepositoryException ex) { throw new CmsException(ex); } catch (Exception e) { throw new CmsException(e); } } public void removeLockFromContentObject(String contentObjectId, String lockToken) { Session session = null; try { session = getSession(); Node contentObjectNode = cmsRepositoryEntityUtils.retrieveUniqueNodeForContentObject(session, contentObjectId); if (contentObjectNode != null && contentObjectNode.isLocked()) { //Deep lock and open-scoped session.getWorkspace().getLockManager().addLockToken(lockToken); session.getWorkspace().getLockManager().unlock(contentObjectNode.getPath()); //Unset contentObject equivalent property if (contentObjectNode.hasProperty(DefinitionReservedName.Locktoken.getJcrName())) { session.getWorkspace().getVersionManager().checkout(contentObjectNode.getPath()); contentObjectNode.getProperty(DefinitionReservedName.Locktoken.getJcrName()).remove(); } session.save(); } }catch(RepositoryException ex) { throw new CmsException(ex); } catch (Exception e) { throw new CmsException(e); } } public boolean isContentObjectLocked(String contentObjectId) { Session session = null; try { session = getSession(); Node contentObjectNode = cmsRepositoryEntityUtils.retrieveUniqueNodeForContentObject(session, contentObjectId); if (contentObjectNode == null){ return false; } return contentObjectNode.isLocked(); }catch(RepositoryException ex) { throw new CmsException(ex); } catch (Exception e) { throw new CmsException(e); } } public void increaseContentObjectViewCounter(String contentObjectId, long counter) { Session session = null; try { session = getSession(); Node contentObjectNode = cmsRepositoryEntityUtils.retrieveUniqueNodeForContentObject(session, contentObjectId); if (contentObjectNode == null){ logger.warn("Unable to find content object with id "+ contentObjectId+". Property 'viewCounter' is not increased"); } else{ session.getWorkspace().getVersionManager().checkout(contentObjectNode.getPath()); contentObjectDao.increaseViewCounter(contentObjectNode, counter); session.save(); } }catch(RepositoryException ex) { logger.error("",ex); throw new CmsException(ex); } catch (Exception e) { logger.error("",e); throw new CmsException(e); } } public List<CmsProperty<?, ?>> loadChildCmsProperty(String childPropertyName, String parentComplexCmsPropertyDefinitionFullPath, String jcrNodeUUIDWhichCorrespondsToParentComplexCmsProperty, String jcrNodeUUIDWhichCorrespondsToContentObejct, RenderProperties renderProperties) throws Exception { CmsPropertyDefinition currentChildPropertyDefinition = contentDefinitionDao.getCmsPropertyDefinition(PropertyPath.createFullPropertyPath(parentComplexCmsPropertyDefinitionFullPath, childPropertyName)); //Since definition does not exist and its parent is a content object type, which //derives from the fact that parent full path has only one level //check if child refers to an aspect if (currentChildPropertyDefinition == null && !parentComplexCmsPropertyDefinitionFullPath.contains(CmsConstants.PERIOD_DELIM)){ currentChildPropertyDefinition = contentDefinitionDao.getAspectDefinition(childPropertyName); //At this point if such a definition exists we cannot tell that the aspect found is actually defined //for the content object which contains parent complex property. //This will be detected once the created template is actually added to the content object } if (currentChildPropertyDefinition == null){ logger.warn("No cms property definition is provided for property {}.{} . ( parent node UUID {} and content object node UUID {} )", new Object[]{parentComplexCmsPropertyDefinitionFullPath, childPropertyName, jcrNodeUUIDWhichCorrespondsToParentComplexCmsProperty, jcrNodeUUIDWhichCorrespondsToContentObejct}); return null; } else{ return lazyComplexCmsPropertyLoader.renderChildProperty(currentChildPropertyDefinition, jcrNodeUUIDWhichCorrespondsToParentComplexCmsProperty, jcrNodeUUIDWhichCorrespondsToContentObejct, renderProperties, getSession(), null); } } public void moveAspectToNativePropertyForAllContentObjectsOFContentType( String aspect, String newPropertyName, String contentType) { if (StringUtils.isBlank(aspect)){ throw new CmsException("No aspect is provided"); } if (StringUtils.isBlank(contentType)){ throw new CmsException("No content type is provided"); } Session session = null; try { session = getSession(); ContentObjectCriteria contentObjectCriteria = CmsCriteriaFactory.newContentObjectCriteria(contentType); CmsQueryResult contentObjectResults = cmsQueryHandler.getNodesFromXPathQuery(session, contentObjectCriteria, true); if (contentObjectResults.getTotalRowCount() > 0){ NodeIterator contentObjectNodesIterator = (NodeIterator) contentObjectResults.getNodeIterator(); int count = 0; while (contentObjectNodesIterator.hasNext()){ Node contentObjectNode = contentObjectNodesIterator.nextNode(); if (contentObjectNode.isNodeType(CmsBuiltInItem.StructuredContentObject.getJcrName())){ if (contentObjectNode.hasProperty(CmsBuiltInItem.Aspects.getJcrName())){ Value[] values = contentObjectNode.getProperty(CmsBuiltInItem.Aspects.getJcrName()).getValues(); logger.info("ContentObjectNode "+contentObjectNode.getPath() + " has aspects "+ printValues(values)); if (values != null){ for (Value value : values){ if (value.getString() != null && value.getString().equals(aspect)){ //Aspect found. Remove it from array JcrValueUtils.removeValue(contentObjectNode, CmsBuiltInItem.Aspects, session.getValueFactory().createValue(aspect), true); count++; if (contentObjectNode.hasProperty(CmsBuiltInItem.Aspects.getJcrName())){ logger.info("After removal ContentObjectNode "+contentObjectNode.getPath() + " has aspects "+ printValues(values)); } else{ logger.info("After removal ContentObjectNode "+contentObjectNode.getPath() + " does not have any aspect "); } //Refactor the property only if the provided is different if (StringUtils.isNotBlank(newPropertyName) && ! newPropertyName.equals(aspect)){ if (!contentObjectNode.hasNode(aspect)){ logger.warn("Although aspect "+ aspect + " was found in "+CmsBuiltInItem.Aspects.getJcrName() + " no child property named after aspect "+ " was found. Check further if there is a bug"); } else{ NodeIterator aspectNodes = contentObjectNode.getNodes(aspect); if (aspectNodes.getSize() > 1){ logger.warn("There are more than one child properties named after aspect "+aspect+ ". This is however not the case." + "Check further if there is a bug. Refactoring will taking place normally as by now this property is a native property" + " and therefore can have multiple occurences. Make sure this is depicted in content type definition of "+contentType); } while (aspectNodes.hasNext()){ Node aspectNode = aspectNodes.nextNode(); session.move(aspectNode.getPath(), contentObjectNode.getPath()+CmsConstants.FORWARD_SLASH+newPropertyName); } } } break; } } } } } else{ throw new CmsException("In a query for content objects of type "+ contentType + " this node "+ contentObjectNode.getPath() + " found in results but it is not a structured content object node"); } } session.save(); logger.info("Successfully move aspect in "+count + " content objects"); } }catch(RepositoryException ex) { throw new CmsException(ex); } catch (Exception e) { throw new CmsException(e); } } private String printValues(Value[] values) throws Exception { if (values == null){ return ""; } StringBuilder stringBuilder = new StringBuilder(); for (Value value : values){ stringBuilder.append(value.getString()+ " "); } return stringBuilder.toString(); } public Node getContentObjectNodeByIdOrSystemName(String contentObjectIdOrSystemName){ try{ Node contentObjectNode = null; if (CmsConstants.UUIDPattern.matcher(contentObjectIdOrSystemName).matches()){ contentObjectNode = cmsRepositoryEntityUtils.retrieveUniqueNodeForContentObject(getSession(), contentObjectIdOrSystemName); if (contentObjectNode != null){ return contentObjectNode; } } else{ ContentObjectCriteria contentObjectCriteria = CmsCriteriaFactory.newContentObjectCriteria(); contentObjectCriteria.addSystemNameEqualsCriterion(contentObjectIdOrSystemName); contentObjectCriteria.setOffsetAndLimit(0, 1); CmsQueryResult nodes = cmsQueryHandler.getNodesFromXPathQuery(getSession(), contentObjectCriteria, true); if (nodes.getTotalRowCount() > 0){ return ((NodeIterator) nodes.getNodeIterator()).nextNode(); } } return null; } catch (Exception e) { throw new CmsException(e); } } @SuppressWarnings("unchecked") public <T> T serializeContentObject(String contentObjectIdOrSystemName, CacheRegion cacheRegion, ResourceRepresentationType<T> contentObjectOutput, List<String> propertyPathsToInclude, FetchLevel fetchLevel, boolean serializeBinaryContent, boolean prettyPrint) { ByteArrayOutputStream os = null; try { //Default values if (contentObjectOutput == null){ contentObjectOutput = (ResourceRepresentationType<T>) ResourceRepresentationType.CONTENT_OBJECT_INSTANCE; } T contentObject = null; //Check cache contentObject = getContentObjectFromcache(contentObjectIdOrSystemName, cacheRegion, contentObjectOutput); if (contentObject != null){ return contentObject; } //Content Object is not in the cache //Continue with the default values if (fetchLevel == null){ fetchLevel = FetchLevel.ENTITY; } propertyPathsToInclude = generateIfNecessaryPropertyPathsWhoseValuesWillBeIncludedInSerialization(fetchLevel, propertyPathsToInclude,contentObjectOutput); Node contentObjectNode = getContentObjectNodeByIdOrSystemName(contentObjectIdOrSystemName); if (contentObjectNode == null){ return generateEmptyOutcome(contentObjectOutput); } if (ResourceRepresentationType.CONTENT_OBJECT_INSTANCE.equals(contentObjectOutput)|| ResourceRepresentationType.CONTENT_OBJECT_LIST.equals(contentObjectOutput)){ RenderProperties renderProperties = new RenderPropertiesImpl(); if (FetchLevel.FULL == fetchLevel){ renderProperties.renderAllContentObjectProperties(true); } contentObject = (T) contentObjectRenderer.render(getSession(), contentObjectNode, renderProperties, new HashMap<String, ContentObjectTypeDefinition>(), new HashMap<String, CmsRepositoryEntity>()); //Load properties if (CollectionUtils.isNotEmpty(propertyPathsToInclude)){ for (String propertyPath : propertyPathsToInclude){ ((ContentObject)contentObject).getCmsProperty(propertyPath); } } //Return appropriate type if (ResourceRepresentationType.CONTENT_OBJECT_LIST.equals(contentObjectOutput )){ //Return type is CmsOutcome. CmsOutcome<ContentObject> outcome = new CmsOutcomeImpl<ContentObject>(1, 0, 1); outcome.getResults().add((ContentObject)contentObject); contentObject = (T) outcome; } } else if (ResourceRepresentationType.XML.equals(contentObjectOutput)|| ResourceRepresentationType.JSON.equals(contentObjectOutput)){ os = new ByteArrayOutputStream(); SerializationConfiguration serializationConfiguration = SerializationConfiguration.object() .prettyPrint(prettyPrint) .representationType(contentObjectOutput) .serializeBinaryContent(serializeBinaryContent) .build(); serializationDao.serializeCmsRepositoryEntity(contentObjectNode, os, CmsEntityType.OBJECT, propertyPathsToInclude, fetchLevel, true, serializationConfiguration); contentObject = (T) new String(os.toByteArray(), "UTF-8"); } else { throw new CmsException("Unsupported resource representation type for content object serialization "+contentObjectOutput); } if (cacheRegion != null){ jcrQueryCacheRegion.cacheContentObject(contentObjectIdOrSystemName, contentObject, cacheRegion, contentObjectOutput.getTypeAsString()); } return contentObject; }catch(RepositoryException ex){ throw new CmsException(ex); } catch (Exception e) { throw new CmsException(e); } finally{ IOUtils.closeQuietly(os); } } private List<String> generateIfNecessaryPropertyPathsWhoseValuesWillBeIncludedInSerialization(FetchLevel fetchLevel, List<String> propertyPathsToInclude, ResourceRepresentationType<?> contentObjectOutput) { //Property Paths to be included in the serialization are ignored id FetchLevel is FULL if (fetchLevel == FetchLevel.FULL){ return null; } if (CollectionUtils.isEmpty(propertyPathsToInclude)){ //FetchLevel is either ENTITY or ENTITY_AND_CHILDREN but user did not specify any properties at all. //In this case profile.title is returned and owner propertyPathsToInclude = new ArrayList<String>(); propertyPathsToInclude.add("profile.title"); //Owner is always serialized when type is CONTENT_OBJECT_INSTANCE or CONTENT_OBJECT_LIST //as it is a special case of a property //However in the cases of XML or JSON, it should be specified as a regular one if (ResourceRepresentationType.XML.equals(contentObjectOutput)|| ResourceRepresentationType.JSON.equals(contentObjectOutput)){ propertyPathsToInclude.add(CmsConstants.OWNER_ELEMENT_NAME); } } return propertyPathsToInclude; } private <T> T getContentObjectFromcache(String contentObjectIdOrSystemName, CacheRegion cacheRegion, ResourceRepresentationType<T> contentObjectOutput) throws Exception { if (cacheRegion != null){ return (T) jcrQueryCacheRegion.getContentObjectFromCache(contentObjectIdOrSystemName, cacheRegion, contentObjectOutput.getTypeAsString()); } return null; } /** * @param contetObjectId * @return */ public ContentObject copyContentObject(String contentObjectId) { if (StringUtils.isBlank(contentObjectId)){ return null; } ContentObject contentObjectToBeCopied = serializeContentObject(contentObjectId, CacheRegion.NONE, ResourceRepresentationType.CONTENT_OBJECT_INSTANCE, null, FetchLevel.FULL, true,false); if (contentObjectToBeCopied == null){ logger.warn("Could not retrieve content object with id {}. Copy operation cannot continue", contentObjectId); return null; } ((ContentObjectImpl)contentObjectToBeCopied).clean(); int index = 0; String systemNameToSearch = null; if (StringUtils.isNotBlank(contentObjectToBeCopied.getSystemName())){ systemNameToSearch = new String(contentObjectToBeCopied.getSystemName().getBytes()); if (systemNameToSearch.startsWith("copy")){ systemNameToSearch = systemNameToSearch.replaceFirst("copy[0-9]*", ""); } //Locate other copies in order to provide the correct copy index ContentObjectCriteria contentObjectCriteria = CmsCriteriaFactory.newContentObjectCriteria(); contentObjectCriteria.addSystemNameContainsCriterion("*"+systemNameToSearch); contentObjectCriteria.setOffsetAndLimit(0, 0); contentObjectCriteria.setCacheable(CacheRegion.NONE); CmsOutcome<ContentObject> outcome = searchContentObjects(contentObjectCriteria, ResourceRepresentationType.CONTENT_OBJECT_LIST); if (outcome != null){ index = (int)outcome.getCount(); } } String newIndexAsString = index == 0 ? "" : String.valueOf(index+1); //Adjust systemName if (contentObjectToBeCopied.getSystemName() == null){ contentObjectToBeCopied.setSystemName("copy"+newIndexAsString); } else{ if (contentObjectToBeCopied.getSystemName().startsWith("copy")){ contentObjectToBeCopied.setSystemName(contentObjectToBeCopied.getSystemName().replaceFirst("copy[0-9]*", "copy"+newIndexAsString)); } else{ contentObjectToBeCopied.setSystemName("copy"+newIndexAsString+contentObjectToBeCopied.getSystemName()); } } //Adjust title StringProperty titleProperty = (StringProperty) contentObjectToBeCopied.getCmsProperty("profile.title"); String title = titleProperty.getSimpleTypeValue(); boolean titleHasChanged = false; for (int i=1;i<=index;i++){ if (title.endsWith(" "+String.valueOf(i))){ titleProperty.setSimpleTypeValue(StringUtils.substringBeforeLast(title, String.valueOf(i))+newIndexAsString); titleHasChanged = true; break; } } if (! titleHasChanged){ titleProperty.setSimpleTypeValue(title+ " "+newIndexAsString); } return contentObjectToBeCopied; } public boolean valueForPropertyExists(String propertyPath, String jcrNodeUUIDWhichCorrespondsToParentComplexCmsProperty) throws Exception { return lazyComplexCmsPropertyLoader.valueForPropertyExists(propertyPath, jcrNodeUUIDWhichCorrespondsToParentComplexCmsProperty, getSession()); } public List<ContentObject> saveContentObjectResourceCollection(Object contentSource, boolean version,boolean updateLastModificationTime, String lockToken) { long start = System.currentTimeMillis(); if (contentSource == null){ return new ArrayList<ContentObject>(); } if (contentSource instanceof String){ logger.debug(" Starting saving content object resource collection."); //Use importer to unmarshal String to ContentObject //and to save it as well. //What is happened is that importDao will create a ContentObject //and will pass it to ContentServiceImpl to save it. //It will end up in this method again as a ContentObject //if it passes the check of SecureContentObjectSaveAspect ImportConfiguration configuration = ImportConfiguration.object() .persist(PersistMode.PERSIST_ENTITY_TREE) .version(version) .updateLastModificationTime(updateLastModificationTime) .build(); return importDao.importResourceCollection((String)contentSource, configuration); } if (! (contentSource instanceof List)){ throw new CmsException("Expecting either String or List<ContentObject> and not "+contentSource.getClass().getName()); } logger.debug(" Starting saving content object collection"); Session session = null; Context context = null; List<ContentObject> contentObjects = (List<ContentObject>) contentSource; try { context = new Context(cmsRepositoryEntityUtils, cmsQueryHandler, getSession()); session = context.getSession(); if (StringUtils.isNotBlank(lockToken)){ session.getWorkspace().getLockManager().addLockToken(lockToken); } for (ContentObject contentObject : contentObjects){ contentObject = saveContentObject(contentObject, version, lockToken, updateLastModificationTime, context); } session.save(); return contentObjects; } catch(CmsException e){ throw e; } catch(Throwable e){ throw new CmsException(e); } finally{ if (context != null){ context.dispose(); context = null; } if (StringUtils.isNotBlank(lockToken)){ try { session.getWorkspace().getLockManager().removeLockToken(lockToken); } catch (RepositoryException e) { logger.error("Lock token "+lockToken+" could not be removed", e); } } logger.debug(" Saved ContentObject collection in {}", DurationFormatUtils.formatDurationHMS(System.currentTimeMillis() - start)); } } public byte[] getBinaryChannelContent( String jcrNodeUUIDWhichCorrespondsToTheBinaryChannel) { Session session = null; if (StringUtils.isBlank(jcrNodeUUIDWhichCorrespondsToTheBinaryChannel)) throw new CmsException("Blank binary channel id"); try { session = getSession(); Node binaryChannelNode = cmsRepositoryEntityUtils.retrieveUniqueNodeForBinaryChannel(session, jcrNodeUUIDWhichCorrespondsToTheBinaryChannel); if (binaryChannelNode == null){ logger.warn("Binary Channel Id {} does not correspond to a valid JCR node"); return null; } if (binaryChannelNode.hasProperty(JcrBuiltInItem.JcrData.getJcrName())){ return (byte[])JcrValueUtils.getObjectValue(binaryChannelNode.getProperty(JcrBuiltInItem.JcrData.getJcrName()).getValue()); } else { return null; } }catch(RepositoryException ex) { throw new CmsException(ex); } catch (Exception e) { throw new CmsException(e); } } }