Java tutorial
/* * Crafter Studio Web-content authoring solution * Copyright (C) 2007-2016 Crafter Software Corporation. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.craftercms.studio.impl.v1.service.clipboard; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; import org.craftercms.studio.api.v1.constant.StudioConstants; import org.craftercms.studio.api.v1.constant.DmConstants; import org.craftercms.studio.api.v1.constant.DmXmlConstants; import org.craftercms.studio.api.v1.content.pipeline.DmContentProcessor; import org.craftercms.studio.api.v1.exception.ContentNotFoundException; import org.craftercms.studio.api.v1.exception.ServiceException; import org.craftercms.studio.api.v1.log.Logger; import org.craftercms.studio.api.v1.log.LoggerFactory; import org.craftercms.studio.api.v1.service.AbstractRegistrableService; import org.craftercms.studio.api.v1.service.GeneralLockService; import org.craftercms.studio.api.v1.service.clipboard.ClipboardService; import org.craftercms.studio.api.v1.service.configuration.ServicesConfig; import org.craftercms.studio.api.v1.service.content.*; import org.craftercms.studio.api.v1.service.dependency.DmDependencyService; import org.craftercms.studio.api.v1.service.event.EventService; import org.craftercms.studio.api.v1.service.objectstate.ObjectStateService; import org.craftercms.studio.api.v1.service.security.SecurityProvider; import org.craftercms.studio.api.v1.service.security.SecurityService; import org.craftercms.studio.api.v1.to.ContentItemTO; import org.craftercms.studio.api.v1.to.ContentTypeConfigTO; import org.craftercms.studio.api.v1.to.DmPasteItemTO; import org.craftercms.studio.impl.v1.util.ContentFormatUtils; import org.craftercms.studio.impl.v1.util.ContentUtils; import org.craftercms.studio.impl.v1.util.PathMacrosTransaltor; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.Node; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class ClipboardServiceImpl extends AbstractRegistrableService implements ClipboardService { protected static final Logger logger = LoggerFactory.getLogger(ClipboardServiceImpl.class); public final static Pattern FILE_PATTERN = Pattern.compile("(.+)-([0-9]+)\\.(.+)"); public final static Pattern FOLDER_PATTERN = Pattern.compile("(.+)-([0-9]+)"); @Override public void register() { getServicesManager().registerService(ClipboardService.class, this); } /** * Entry point for all past operation * * @param pasteItems - items that needs to be pasted in the target location * @param destination - target location where the items have to be pasted * @cut - is it a cut/paste or a copy/paste operation * */ public List<String> paste(final String site, List<Map<String, String>> pasteItems, final String destination, boolean cut) throws ServiceException { if (cut) { return cutPaste(site, pasteItems, destination); } return copyPaste(site, pasteItems, destination); } /** * Duplicate the content (i.e create a copy of a content at the same location by providing auto-generated uri) * */ @Override public String duplicate(String site, String path, String source) throws ServiceException { return _duplicate(site, path, false, source); } protected String _duplicate(String site, String path, boolean toDraft, String source) throws ServiceException { String user = securityService.getCurrentUser(); try { ContentItemTO item = contentService.getContentItem(site, path); if (item != null) { ContentTypeConfigTO config = getContentTypeConfig(site, path); boolean contentAsFolder = (config != null) ? config.isContentAsFolder() : false; if (!contentAsFolder && path.endsWith(DmConstants.INDEX_FILE)) { contentAsFolder = true; } String namePath = path; // if the content is being saved as folder, the name path will be the folder name if (contentAsFolder && path.endsWith(DmConstants.INDEX_FILE)) { namePath = namePath.replace("/" + DmConstants.INDEX_FILE, ""); } String name = contentService.getNextAvailableName(site, namePath); InputStream content = getDuplicatedContent(site, user, source, name, contentAsFolder); String fileName = (contentAsFolder) ? DmConstants.INDEX_FILE : name; String newPath = namePath.substring(0, namePath.lastIndexOf("/")); newPath = (contentAsFolder) ? newPath + "/" + name : newPath; String copiedPath = copy(site, user, null, path, newPath, fileName, content, toDraft, DmContentLifeCycleService.ContentLifeCycleOperation.DUPLICATE); ContentItemTO itemTo = contentService.getContentItem(site, newPath + "/" + fileName); objectStateService.insertNewEntry(site, newPath + "/" + fileName); return newPath + "/" + fileName; } else { throw new ContentNotFoundException(path + " does not exist."); } } finally { } } /** * Copy the given node to the destination node * Provide new pageId/groupId to XMLs * Make copies of dependencies if required * */ protected String copy(String site, String user, SourcePageInfo sourcePageInfo, String path, String destination, String destinationFileName, InputStream content, boolean isDraft, DmContentLifeCycleService.ContentLifeCycleOperation writeOperation) throws ServiceException { if (sourcePageInfo == null) { sourcePageInfo = new SourcePageInfo(path); } ContentTypeConfigTO config = getContentTypeConfig(site, path); if (content == null) { content = contentService.getContent(site, path); } if (path.endsWith(DmConstants.XML_PATTERN)) { if (sourcePageInfo.getParams() == null) { Map<String, String> params = contentItemIdGenerator.getIds();//page and groud id sourcePageInfo.setParams(params); } Map<String, String> dependencies = copyDependencies(site, user, sourcePageInfo, path, isDraft); //update content with all params content = updateContent(site, path, content, sourcePageInfo.getParams(), dependencies); } String contentType = config == null ? null : config.getName(); String fileName = (StringUtils.isEmpty(destinationFileName)) ? ContentUtils.getPageName(path) : destinationFileName; writeContent(site, destination, fileName, user, content, contentType, false, writeOperation); eventService.firePreviewSyncEvent(site); return destination + "/" + fileName; } /** * @param site * @param path * @param content * @param writeOperation * @throws ServiceException */ protected void writeContent(String site, String path, String fileName, String user, InputStream content, String contentType, boolean edit, DmContentLifeCycleService.ContentLifeCycleOperation writeOperation) throws ServiceException { Map<String, String> params = new HashMap<String, String>(); params.put(DmConstants.KEY_SITE, site); params.put(DmConstants.KEY_PATH, path); params.put(DmConstants.KEY_FILE_NAME, fileName); params.put(DmConstants.KEY_USER, user); params.put(DmConstants.KEY_CONTENT_TYPE, contentType); params.put(DmConstants.KEY_CREATE_FOLDERS, "true"); params.put(DmConstants.KEY_EDIT, Boolean.valueOf(edit).toString()); params.put(DmConstants.KEY_ACTIVITY_TYPE, "false"); params.put(DmConstants.KEY_SKIP_CLEAN_PREVIEW, "true"); params.put(DmConstants.KEY_COPIED_CONTENT, "true"); params.put(DmConstants.CONTENT_LIFECYCLE_OPERATION, writeOperation.toString()); String id = site + ":" + path + ":" + fileName + ":" + contentType; // processContent will close the input stream if (!StringUtils.isEmpty(contentType)) { contentService.processContent(id, content, true, params, DmConstants.CONTENT_CHAIN_FORM); } else { if (fileName.endsWith(DmConstants.XML_PATTERN)) { contentService.processContent(id, content, true, params, DmConstants.CONTENT_CHAIN_FORM); } else { contentService.processContent(id, content, false, params, DmConstants.CONTENT_CHAIN_ASSET); } } } /** * * Looks for dependencies in the source file and optionally (if mentioned in the config) create copies * * * @param site * @param user */ protected Map<String, String> copyDependencies(String site, String user, SourcePageInfo sourcePageInfo, String dependencyPath, boolean isPreview) throws ServiceException { //get the dependencies path that are not shared Map<String, String> copyDependencies = dmDependencyService.getCopyDependencies(site, sourcePageInfo.getPath(), dependencyPath); Map<String, String> copiedDependenices = new HashMap<String, String>(); for (String path : copyDependencies.keySet()) { String target = copyDependencies.get(path); String destinationPath = PathMacrosTransaltor.resolvePath(target, sourcePageInfo.getParams()); //get the new target location //make copy of each dependency to the new location String copiedPath = copy(site, user, sourcePageInfo, path, destinationPath, null, null, isPreview, DmContentLifeCycleService.ContentLifeCycleOperation.COPY); copiedDependenices.put(path, copiedPath); } return copiedDependenices; } /** * Update content only for Index pages * * @param path * @param is * @param params * @param dependencies * @return * @throws ServiceException */ protected InputStream updateContent(String site, String path, InputStream is, Map<String, String> params, Map<String, String> dependencies) throws ServiceException { if (path.endsWith(DmConstants.XML_PATTERN)) { Document document; try { document = ContentUtils.convertStreamToXml(is); } catch (DocumentException e) { throw new ServiceException("Error while updating content", e); } if (document != null) { //update pageId and groupId with the new one Element root = document.getRootElement(); Node pageIdNode = root.selectSingleNode("//" + DmXmlConstants.ELM_PAGE_ID); if (pageIdNode != null) { ((Element) pageIdNode).setText(params.get(DmConstants.KEY_PAGE_ID)); } Node groupIdNode = root.selectSingleNode("//" + DmXmlConstants.ELM_GROUP_ID); if (pageIdNode != null) { ((Element) groupIdNode).setText(params.get(DmConstants.KEY_PAGE_GROUP_ID)); } is = dmDependencyService.replaceDependencies(site, document, dependencies); } } return is; } /** * get the duplicated content * * @param site * @param path * @param name * @param contentAsFolder * @return duplicated content * @throws ContentNotFoundException */ protected InputStream getDuplicatedContent(String site, String user, String path, String name, boolean contentAsFolder) throws ContentNotFoundException { // if it is XML, change the internal name with the next number if (path.endsWith(DmConstants.XML_PATTERN)) { Document document = null; try { document = contentService.getContentAsDocument(site, path); } catch (DocumentException e) { logger.error("Error getting xml document for following site: " + site + " path: " + path); } if (document != null) { Pattern pattern = (contentAsFolder) ? FOLDER_PATTERN : FILE_PATTERN; final Matcher m = pattern.matcher(name); if (m.matches()) { String number = m.group(2); Element root = document.getRootElement(); // set internal name to be the same as duplicate content Node nameNode = root.selectSingleNode("//" + DmXmlConstants.ELM_INTERNAL_NAME); if (nameNode != null) { String nameValue = nameNode.getText(); ((Element) nameNode).setText(nameValue + " " + number); } Node fileNameNode = root.selectSingleNode("//" + DmXmlConstants.ELM_FILE_NAME); if (fileNameNode != null) { String fileName = (contentAsFolder) ? DmConstants.INDEX_FILE : name; ((Element) fileNameNode).setText(fileName); } // set content history - modifier Node modifierNode = root.selectSingleNode("//" + DmXmlConstants.ELM_LAST_MODIFIED_BY); if (modifierNode != null) { ((Element) modifierNode).setText(user); } // set content history - modified date Node modifiedDateNode = root.selectSingleNode("//" + DmXmlConstants.ELM_LAST_MODIFIED_DATE); if (modifiedDateNode != null) { SimpleDateFormat format = new SimpleDateFormat(StudioConstants.DATE_PATTERN_MODEL); String date = ContentFormatUtils.formatDate(format, new Date()); String formDate = ContentFormatUtils.convertToFormDate(date); ((Element) modifiedDateNode).setText(formDate); } } return ContentUtils.convertDocumentToStream(document, StudioConstants.CONTENT_ENCODING); } else { return null; } } else { // otherwise, return the content as is return contentService.getContent(site, path); } } /** * get content type configuration of the content at the given path * * @param site * @param sub * @param path * @return content type configuration */ protected ContentTypeConfigTO getContentTypeConfig(String site, String path) { try { return contentTypeService.getContentTypeByRelativePath(site, path); } catch (ServiceException e) { // ignore the error logger.warn("No content type found for " + path); } return null; } /** * Cut/Paste calls the Rename service <link>WcmRenameService</link> */ protected List<String> cutPaste(final String site, List<Map<String, String>> pasteItems, final String destination) throws ServiceException { for (final Map<String, String> pasteItem : pasteItems) { String path = pasteItem.get("uri"); path = FilenameUtils.normalize(path); objectStateService.setSystemProcessing(site, path, true); String destinationUri = destination; if (destination.endsWith(DmConstants.INDEX_FILE)) { destinationUri = ContentUtils.getParentUrl(destinationUri); } destinationUri = destinationUri + "/" + ContentUtils.getPageName(path.replace("/" + DmConstants.INDEX_FILE, "")); String destinationFinalUri = destinationUri; String lockKey = site + ":" + path; generalLockService.lock(lockKey); String lockKeyDest = site + ":" + destinationUri; generalLockService.lock(lockKeyDest); try { if (contentService.contentExists(site, destinationUri)) { throw new ServiceException("Content already exists [" + site + ":" + destinationUri + "]"); } else { logger.error("Destination URI: " + destinationUri); dmRenameService.rename(site, path, destinationUri, false); updateFileWithNewNavOrder(site, destinationUri);//only for cut/paste will need to provide new navorder value right here since it doesnt go through FormContentProcessor ContentItemTO itemTO = contentService.getContentItem(site, destinationUri); destinationFinalUri = itemTO.getUri(); objectStateService.transition(site, itemTO, org.craftercms.studio.api.v1.service.objectstate.TransitionEvent.SAVE); } } finally { objectStateService.setSystemProcessing(site, path, false); generalLockService.unlock(lockKey); generalLockService.unlock(lockKeyDest); } } return null; } /** * Update the XML content with new Page Nav Order during copy/paste * * @param site * @param sub * @param path */ protected void updateFileWithNewNavOrder(String site, String path) { if (!path.endsWith(DmConstants.XML_PATTERN)) { path = path + "/" + DmConstants.INDEX_FILE; } try { if (contentService.contentExists(site, path)) { if (path.endsWith(DmConstants.XML_PATTERN)) { Document document = contentService.getContentAsDocument(site, path); DmPageNavigationOrderService dmPageNavigationOrderService = getService( DmPageNavigationOrderService.class); dmPageNavigationOrderService.addNavOrder(site, path, document); InputStream content = ContentUtils.convertDocumentToStream(document, StudioConstants.CONTENT_ENCODING); updateFileDirect(site, path, content); } } } catch (Exception e) { logger.warn("Error while update file with new Nav order " + path, e); } } /** * Update file directly rather than going through the pipeline processor * TODO - remove this to use the pipeline processor */ protected void updateFileDirect(String site, String relativePath, InputStream input) throws ServiceException { try { contentService.writeContent(site, relativePath, input); } finally { ContentUtils.release(input); } } /** * Copy/Paste called with items to be pasted at the destination */ protected List<String> copyPaste(final String site, List<Map<String, String>> pasteItems, final String destination) throws ServiceException { final String user = securityService.getCurrentUser(); List<String> copiedItems = new ArrayList<>(); if (pasteItems != null && pasteItems.size() > 0) { ContentItemTO destItem = contentService.getContentItem(site, destination); if (destItem == null) { throw new ServiceException("Error while pasting content. " + destination + " does not exist."); } String destNodePath = destination; if (!destItem.isFolder()) { if (destItem.getName().equals(DmConstants.INDEX_FILE)) { destNodePath = destItem.getPath(); } else { String folderPath = writeProcessor.fileToFolder(site, destNodePath); destItem = contentService.getContentItem(site, folderPath); destNodePath = folderPath; } } else if (destItem.isPage()) { if (destItem.getName().equals(DmConstants.INDEX_FILE)) { destNodePath = destItem.getPath(); } } else if (destItem.isFolder()) { destNodePath = destItem.getUri(); } for (final Map<String, String> pasteItem : pasteItems) { DmPasteItemTO dmPasteItem = new DmPasteItemTO(); String pasteItemUri = pasteItem.get("uri"); pasteItemUri = pasteItemUri.replaceAll("//", "/"); dmPasteItem.setUri(pasteItemUri); dmPasteItem.setDeep(Boolean.parseBoolean(pasteItem.get("deep"))); if (hasSameChild(pasteItemUri, destItem, site)) { //if the parent folder has the same content then duplicate the content duplicateContent(site, destination, user, destItem, pasteItemUri); } else { copyContent(site, user, dmPasteItem, destNodePath, copiedItems); } } } return copiedItems; } /** * CopyContent creates copy of the item at the destination * If there are item.getChildren it recursively gets called to create copy of them as well * * @throws ServiceException */ protected void copyContent(String site, String user, DmPasteItemTO pasteItem, String destination, List<String> copiedItems) throws ServiceException { String path = pasteItem.getUri(); boolean deep = true; try { ContentItemTO contentItem = contentService.getContentItemTree(site, path, 1); if (contentItem == null) { throw new ServiceException("Error while pasting content. " + path + " does not exist."); } String nodePath = path; if (contentItem.isFolder()) { copyChildren(site, user, path, contentItem, destination + "/" + contentItem.getName(), pasteItem, deep, true, copiedItems); if (contentItem.getChildren() == null || contentItem.getChildren().size() == 0) { contentService.copyContent(site, path, destination); } } else if (contentItem.getName().equals(DmConstants.INDEX_FILE)) { String folderPath = contentItem.getPath(); if (folderPath.endsWith("/")) { folderPath = folderPath.substring(0, folderPath.length() - 1); } String folderName = folderPath.substring(folderPath.lastIndexOf("/") + 1); path = path.replace("/" + DmConstants.INDEX_FILE, ""); copyChildren(site, user, path, contentItem, destination + "/" + folderName, pasteItem, deep, false, copiedItems); // copy the index file String copiedPath = copy(site, user, null, contentItem.getUri(), destination + "/" + folderName, null, null, false, DmContentLifeCycleService.ContentLifeCycleOperation.COPY); objectStateService.insertNewEntry(site, copiedPath); copiedItems.add(copiedPath); } else { String copiedPath = copy(site, user, null, nodePath, destination, null, null, false, DmContentLifeCycleService.ContentLifeCycleOperation.COPY); objectStateService.insertNewEntry(site, copiedPath); copiedItems.add(copiedPath); } } catch (Exception e) { throw new ServiceException("Error while pasting " + path, e); } } /** * Make copy of the child items * */ protected void copyChildren(String site, String user, String parentPath, ContentItemTO parentContentItem, String destination, DmPasteItemTO parentItem, boolean deep, boolean copyIndex, List<String> copiedItems) throws ServiceException { if (deep) { ContentItemTO parentItemTree = contentService.getContentItemTree(site, parentContentItem.getUri(), 1); /** * first paste all the children and then at last paste current node. */ for (ContentItemTO child : parentItemTree.getChildren()) { copyItem(site, user, child.getPath(), destination, deep, copiedItems, child); } } else { if (parentContentItem.getChildren() != null && parentContentItem.getChildren().size() > 0) { for (DmPasteItemTO childPasteItem : parentItem.getChildren()) { copyContent(site, user, childPasteItem, destination, copiedItems); } } } } /** * Ultimately calls <code>copy</code> with required parameters */ protected void copyItem(String site, String user, String parentPath, String destination, boolean deep, List<String> copiedItems, ContentItemTO childItem) throws ServiceException { DmPasteItemTO childPasteItem = new DmPasteItemTO(); String childUri = parentPath + "/" + childItem.getName(); childPasteItem.setUri(childUri); childPasteItem.setDeep(deep); copyContent(site, user, childPasteItem, destination, copiedItems); } /** * check if pasting to the same destination * 1) path = destination * 2) path's directory path = destination (either folder path or index.xml) * * @param srcPath * @param destination * @param site * @return true if the same destination */ protected boolean hasSameChild(String srcPath, ContentItemTO destination, String site) { String originalDirectory = ContentUtils.getParentUrl(srcPath); ContentItemTO itemTree = contentService.getContentItemTree(site, destination.getUri(), 2); String originalDirectoryPath = originalDirectory; if (!srcPath.endsWith("/" + DmConstants.INDEX_FILE)) { originalDirectoryPath = srcPath; } for (ContentItemTO child : itemTree.getChildren()) { String childUri = child.getUri().replace("/" + DmConstants.INDEX_FILE, ""); String childName = ContentUtils.getPageName(childUri); if (originalDirectoryPath.endsWith("/" + childName)) { return true; } } return false; } /** * * If the pasted parent page already exist then duplicate the page and create copies of all the children * * @param site * @param destination * @param user * @param destNode * @param item * @throws ServiceException */ protected void duplicateContent(final String site, final String destination, final String user, ContentItemTO destItem, final String pasteItem) throws ServiceException { ContentItemTO srcItem = contentService.getContentItem(site, pasteItem); final String destRelLoc = getDestPath(site, destItem.getUri(), pasteItem, srcItem); //Create copies of the child items final String newPath = duplicate(site, destRelLoc, pasteItem); } protected String getDestPath(String site, String destination, String pasteItem, ContentItemTO srcItem) { if (destination.endsWith("/" + DmConstants.INDEX_FILE)) { destination = destination.replace("/" + DmConstants.INDEX_FILE, ""); } String destRelLoc = destination + '/' + srcItem.getName(); if (srcItem.getName().equals(DmConstants.INDEX_FILE)) { srcItem = contentService.getContentItem(site, pasteItem.replace("/" + srcItem.getName(), "")); destRelLoc = destination + "/" + srcItem.getName() + "/" + DmConstants.INDEX_FILE; } return destRelLoc; } public class SourcePageInfo { protected String path; //parent Page path incase of dependencies protected Map<String, String> params; //keep track of source page Ids required by the dependencies public SourcePageInfo(String path) { super(); this.path = path; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public Map<String, String> getParams() { return params; } public void setParams(Map<String, String> params) { this.params = params; } } public DmContentProcessor getWriteProcessor() { return writeProcessor; } public void setWriteProcessor(DmContentProcessor writeProcessor) { this.writeProcessor = writeProcessor; } public ServicesConfig getServicesConfig() { return servicesConfig; } public void setServicesConfig(ServicesConfig servicesConfig) { this.servicesConfig = servicesConfig; } public ContentService getContentService() { return contentService; } public void setContentService(ContentService contentService) { this.contentService = contentService; } public ObjectStateService getObjectStateService() { return objectStateService; } public void setObjectStateService(ObjectStateService objectStateService) { this.objectStateService = objectStateService; } public DmRenameService getDmRenameService() { return dmRenameService; } public void setDmRenameService(DmRenameService dmRenameService) { this.dmRenameService = dmRenameService; } public SecurityService getSecurityService() { return securityService; } public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } public ContentTypeService getContentTypeService() { return contentTypeService; } public void setContentTypeService(ContentTypeService contentTypeService) { this.contentTypeService = contentTypeService; } public ContentItemIdGenerator getContentItemIdGenerator() { return contentItemIdGenerator; } public void setContentItemIdGenerator(ContentItemIdGenerator contentItemIdGenerator) { this.contentItemIdGenerator = contentItemIdGenerator; } public DmDependencyService getDmDependencyService() { return dmDependencyService; } public void setDmDependencyService(DmDependencyService dmDependencyService) { this.dmDependencyService = dmDependencyService; } public GeneralLockService getGeneralLockService() { return generalLockService; } public void setGeneralLockService(GeneralLockService generalLockService) { this.generalLockService = generalLockService; } public SecurityProvider getSecurityProvider() { return securityProvider; } public void setSecurityProvider(SecurityProvider securityProvider) { this.securityProvider = securityProvider; } public EventService getEventService() { return eventService; } public void setEventService(EventService eventService) { this.eventService = eventService; } protected DmContentProcessor writeProcessor; protected ServicesConfig servicesConfig; protected ContentService contentService; protected ObjectStateService objectStateService; protected DmRenameService dmRenameService; protected SecurityService securityService; protected ContentTypeService contentTypeService; protected ContentItemIdGenerator contentItemIdGenerator; protected DmDependencyService dmDependencyService; protected GeneralLockService generalLockService; protected SecurityProvider securityProvider; protected EventService eventService; }