Monday, June 1, 2020

Change Type of custom content types


If you have to change the content type (basically cm:content to a custom content type),  then there is an OOTB action provided in Share document library. 

This is very simple use case. But there could be a situation where you have multiple custom content types or folder types such as:

<types>	
	<type name="demo:whitePaper">
		<title>WhitePaper document</title>
		<description>A WhitePaper document</description>
		<parent>cm:content</parent>
	</type>
	
	<type name="demo:supportingDoc">
		<title>Supporting document</title>
		<description>A supporting document</description>
		<parent>cm:content</parent>
	</type>
</types>
Here, both demo:whitePaper and demo:supportingDoc custom content types are defined by extending "cm:content" type.
-----------------------------------------------------------------

With OOTB box change type action, if you have created a content/file which is of type "cm:content" then 
you can change the type from cm:content to demo:whitePaper or demo:supportingDoc based on the configuration made
in share config (share-config-custom.xml).

Example of share-config-custom.xml config:
<config evaluator="string-compare" condition="DocumentLibrary">
	<aspects>
		<visible/>
		<addable/>
		<removable/>
	</aspects>
	<types>
		<type name="cm:content">
			<subtype name="demo:whitePaper"/>
			<subtype name="demo:supportingDoc"/>
		</type>
	</types>
</config>
Change Type action uses a repository webscript, it calls: 
Document List Component - type submit at "/slingshot/doclib/type/node/{store_type}/{store_id}/{id}" url.

But if you have a requirement to allow changing from demo:whitePaper to demo:supportingDoc or vice-versa,
then OOTB box action will not work. However, you can create a custom webscript/action to achieve this use case
or extend the OOTB "Document List Component - type submit" webscript to allow handling custom content types.

Here, we will try to extend the "Document List Component - type submit" webscript to achieve our goal.

Follow the below given steps to extend the Change Type webscript as mentioned above:

  • Create "type.post.desc.xml" webscript definition file under extension/templates directory in following directory structure:
extension/templates/webscripts/org/alfresco/slingshot/documentlibrary/type.post.desc.xml

  • Add following content in the "type.post.desc.xml" file:
<!-- Extended the webScript for custom handling of change-type action -->
<webscript>
    <shortname>type</shortname>
    <description>Document List Component - type submit</description>
    <url>/slingshot/doclib/type/node/{store_type}/{store_id}/{id}</url>
    <format default="json">argument</format>
    <authentication>user</authentication>
    <transaction>required</transaction>
    <lifecycle>internal</lifecycle>
</webscript>

  • Create a java class, name it e.g.: "ChangeTypeWebscript",
This class extends "org.springframework.extensions.webscripts.AbstractWebScript"
  • Add following code in this class:
/*
 * Created By: Abhinav Kumar Mishra
 * Copyright &copy; 2020. Abhinav Kumar Mishra. 
 * All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.abhinavmishra14.webscript;

import java.io.IOException;

import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.extensions.webscripts.AbstractWebScript;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;

/**
 * The Class ChangeTypeWebscript.<br>
 * Extension of OOTB change type WebScript.
 */
public class ChangeTypeWebscript extends AbstractWebScript {

	/** The Constant LOGGER. */
	private static final Logger LOGGER = LoggerFactory.getLogger(ChangeTypeWebscript.class);

	/** The Constant TYPE_PARAM_KEY. */
	private static final String TYPE_PARAM_KEY = "type";

	/** The Constant STORE_TYPE. */
	private static final String STORE_TYPE = "store_type";

	/** The Constant STORE_ID. */
	private static final String STORE_ID = "store_id";

	/** The Constant PARAM_ID. */
	private static final String PARAM_ID = "id";

	/** The Constant CONTENT_LENGTH. */
	private static final String CONTENT_LENGTH = "Content-Length";

	/** The Constant ENCODING_UTF_8. */
	private static final String ENCODING_UTF_8 = "UTF-8";

	/** The Constant CURRENT_KEY. */
	private static final String CURRENT_KEY = "current";

	/** The Constant CACHE_CONTROL. */
	private static final String CACHE_CONTROL = "cache-Control";
             /** The Constant NO_CACHE. */
             private static final String NO_CACHE = "no-cache"; 
	/** The Constant EXPIRES. */
	private static final String EXPIRES = "Expires";

	/** The Constant PRAGMA. */
	private static final String PRAGMA = "Pragma";

	/** The namespace service. */
	private final transient NamespaceService namespaceService;

	/** The node service. */
	private final transient NodeService nodeService;

	/**
	 * The Constructor.
	 *
	 * @param namespaceService the namespace service
	 * @param nodeService      the node service
	 */
	public ChangeTypeWebscript(final NamespaceService namespaceService, final NodeService nodeService) {
		super();
		this.namespaceService = namespaceService;
		this.nodeService = nodeService;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.extensions.webscripts.WebScript#execute(org.
	 * springframework.extensions.webscripts.WebScriptRequest,
	 * org.springframework.extensions.webscripts.WebScriptResponse)
	 */
	@Override
	public void execute(final WebScriptRequest request, final WebScriptResponse response) throws IOException {
		QName targetType = null;
		try {
		   final JSONObject requestPayload = new JSONObject(request.getContent().getContent());
		   targetType = getTargetType(requestPayload);
	           LOGGER.info("Changing type, the targetType is: {}", targetType);
		   final NodeRef nodeRef = getNodeRef(request);
		   nodeService.setType(nodeRef, targetType);
		   final QName changedType = nodeService.getType(nodeRef);
		   writeResponse(response, changedType.getPrefixedQName(namespaceService));
		} catch (JSONException excp) {
		   LOGGER.error("Error occurred while changing the type {}", targetType, excp);
		   throw new WebScriptException("Change type failed!, targetType was: " + targetType, excp);
		}
	}

	/**
	 * Gets the target type.
	 *
	 * @param payload the payload
	 * @return the target type
	 */
	private QName getTargetType(final JSONObject payload) {
		try {
		  final String type = payload.getString(TYPE_PARAM_KEY);
		  QName qname;
		  if (type.indexOf(String.valueOf(QName.NAMESPACE_BEGIN)) != -1) {
		    qname = QName.createQName(type);
		  } else {
		    qname = QName.createQName(type, namespaceService);
		  }
		  return qname;
		} catch (JSONException jsonex) {
		   throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR,
			 "Error occurred while extracting the target type from input json payload", jsonex);
		}
	}

	/**
	 * Write response.
	 *
	 * @param response    the response
	 * @param changedType the changed type
	 * @throws IOException the IO exception
	 */
	private void writeResponse(final WebScriptResponse response, final QName changedType) throws IOException {
		try {
		  final JSONObject responsePayload = new JSONObject();
		  responsePayload.put(CURRENT_KEY, changedType.getPrefixString());
		  writeResponse(response, responsePayload, false, HttpStatus.SC_OK);
		} catch (JSONException jsonex) {
		  throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR,
		    "Error occurred while writing json response for changedType: " + changedType, jsonex);
		}
	}

	/**
	 * Gets the node ref.
	 *
	 * @param request the request
	 * @return the node ref
	 */
	private NodeRef getNodeRef(final WebScriptRequest request) {
	  final String storeType = getParam(request, STORE_TYPE);
	  final String storeId = getParam(request, STORE_ID);
	  final String identifier = getParam(request, PARAM_ID);
	  return new NodeRef(storeType, storeId, identifier);
	}

	/**
	 * Gets the param.
	 *
	 * @param request   the request
	 * @param paramName the param name
	 * @return the param
	 */
	private String getParam(final WebScriptRequest request, final String paramName) {
	  final String value = StringUtils.trimToNull(request.getServiceMatch().getTemplateVars().get(paramName));
	  if (StringUtils.isBlank(value)) {
	     throw new WebScriptException(Status.STATUS_BAD_REQUEST,
                    String.format("Value for param '%s' is missing, Please provide a valid input", paramName));
	  }
	  return value;
	}

	/**
	 * Write response.
	 * @param response           the response
	 * @param jsonObject         the json object
	 * @param clearCache         the clear cache
	 * @param responseStatusCode the response status code
	 * @throws IOException Signals that an I/O exception has occurred.
	 */
	private void writeResponse(final WebScriptResponse response, final JSONObject jsonObject, 
		final boolean clearCache, final int responseStatusCode) throws IOException {
	 final int length = jsonObject.toString().getBytes(ENCODING_UTF_8).length;
	 response.setContentType(MimetypeMap.MIMETYPE_JSON);
	 response.setContentEncoding(ENCODING_UTF_8);
	 response.addHeader(CONTENT_LENGTH, String.valueOf(length));
	 if (clearCache) {
	  response.addHeader(CACHE_CONTROL, NO_CACHE);
               //Calculate the expires date as per you need, i have kept a regular date for example purpose. 
	  response.addHeader(EXPIRES, "Thu, 04 Jan 2020 00:00:00 EDT");
	  response.addHeader(PRAGMA, NO_CACHE);
	 }
	  response.setStatus(responseStatusCode);
	  response.getWriter().write(jsonObject.toString());
	}
}
  • Add the following bean definition in *-context.xml (e.g. webscript-context.xml):
<bean id="webscript.org.alfresco.slingshot.documentlibrary.type.post"
		class="com.github.abhinavmishra14.webscript.ChangeTypeWebscript" parent="webscript">
    <constructor-arg ref="NamespaceService" />
    <constructor-arg ref="NodeService" />
  </bean>
  • To test whether change type is working or not, let's update the share-config-custom.xml file under
<config evaluator="string-compare" condition="DocumentLibrary"> to add type,sub-type config.
Add following config:
<config evaluator="string-compare" condition="DocumentLibrary">
	<aspects>
	  <visible/>
	  <addable/>
	  <removable/>
	</aspects>
	<types>
		<type name="cm:content">
		  <subtype name="demo:whitePaper"/>
		  <subtype name="demo:supportingDoc"/>
		</type>
		<type name="demo:whitePaper">
		  <subtype name="demo:supportingDoc"/>
		</type>
		<type name="demo:supportingDoc">
		  <subtype name="demo:whitePaper"/>
		</type>
	</types>
</config>
  •  Restart the repository and share. Select a file in your document library and click "Change Type" to use extended action.


-----------------------------------------------------------------------------------------------------------------------------

If you prefer to change the type via Repository JavaScript webscript, you can use the below given script as well:

  • Create "change-type.post.js" under "Repository> Data Dictionary> Web Scripts Extensions" folder.
Assuming you know the nodeRef of the actionable node for which you want to change the type. You can find more info here

    function main(){
    
    	var nodeRefStr = args["nodeRef"]; //Node ref of the corrupted folder (An assumption for the script)
    	var qNameToChangeType = args["qNameToChangeType"]; //Full qualified QName e.g.: {http://www.alfresco.org/model/content/1.0}folder
    	try {
    		var node=search.findNode(nodeRefStr);
    		logger.log("NodeRef: "+ node.nodeRef+" | Name: "+node.name)
    
    		var ctxt = Packages.org.springframework.web.context.ContextLoader.getCurrentWebApplicationContext();
    		var nodeService =  ctxt.getBean('NodeService', org.alfresco.service.cmr.repository.NodeService);
    		var QName = Packages.org.alfresco.service.namespace.QName;
    
    		var nodeTypeFolder = QName.createQName(qNameToChangeType);
    
    		nodeService.setType(node.nodeRef, nodeTypeFolder);
    		model.result = "Success!";
    	} catch (ex) {
    	   logger.log("Exception occurred: " + ex.message);
    	   model.result = ex.message;
    	}
    }
    
    main();
    
  • Create "change-type.post.desc.xml" under "Repository> Data Dictionary> Web Scripts Extensions" folder.
  • <webscript>
      <shortname>Change Type</shortname>
      <description>Change type of a node
    	<![CDATA[ Sample request: http://localhost:8080/alfresco/service/change-type?nodeRef=workspace://SpacesStore/e0df9384-9472-472e-95c0-2e091f452700&qNameToChangeType={http://www.alfresco.org/model/content/1.0}folder]]>
      </description>
      <url>/change-type?nodeRef={nodeRef}&amp;qNameToChangeType={qNameToChangeType}</url>
      <authentication>admin</authentication>
    </webscript>
    
  • Create "change-type.post.html.ftl" under "Repository> Data Dictionary> Web Scripts Extensions" folder.
  • <html>
    <head>
    <title>Change type result</title>
    </head>
    <body>
    	<#if result??>
    	    ${result}
    	</#if>
    </body>
    </html>
    
-----------------------------------------------------------------------------------------------------------------------------

If you prefer to change the type via Javascript console and want to change the type of multiple nodes in a given site,
you can use the below given script:

var inputNodetype = "cm:content"; //Input content type or folder type. In this example it's cm:content and target is demo:whitePaper content type

var siteShortName = "test"; //Site short name, Site name is 'Test', ShortName: 'test'

var targetFullQualifiedNodetype = "{http://www.github.com/abhinavmishra14/model/demo/1.0}whitePaper"; //This must be fully qualified

var folderPath = ""; //If you want to drill down search to a specific folder path. e.g. 'Assets/images' folder In documentLibrary


var additionalQuery = ""; //for example, you can also pass any query like '=@cm\\:name:"*Test" (node name ends with Test)'. 
                          //This query is appended to final search query

var skipCount = 0;
var maxCount = 1000;


var query = buildQuery(siteShortName, inputNodetype, folderPath, additionalQuery);
var page = {
	skipCount: parseInt(skipCount),
	maxItems: parseInt(maxCount)
};

var searchQuery = {
	query: query,
	language: "fts-alfresco",
	page: page
};

logger.log("Executing SearchQuery: " + query);

var foundNodes = search.query(searchQuery);
logger.log("Total nodes found: " + foundNodes.length);
	

for each(node in foundNodes) {
  var nodeName = node.properties.name;
  var nodeType = node.type;
	
  logger.log("NodeName: " + nodeName + " | NodeType: " + nodeType);

  if(node.typeShort == inputNodetype) {
	  logger.log("Changing type for node: '"+nodeName+"'");
	  
	  //Get the node service context
	  var ctxt = Packages.org.springframework.web.context.ContextLoader.getCurrentWebApplicationContext();
	  var nodeService =  ctxt.getBean('NodeService', org.alfresco.service.cmr.repository.NodeService);
	  var QName = Packages.org.alfresco.service.namespace.QName;
	  
	  var targetNodeType = QName.createQName(targetFullQualifiedNodetype);
	  nodeService.setType(node.nodeRef, targetNodeType);
	  
	  logger.log("Change type to: '"+targetNodeType+"' has been completed.");
  }
  
}

function buildQuery(siteShortName, inputNodetype, folderPath, additionalQuery) {
    var query = 'PATH:"/app:company_home/st:sites/cm:' + siteShortName;

    if (!!folderPath) { //if not null then process
         query = query + '/cm:documentLibrary/';
         var pathTokens = folderPath.split('/');
         for (var each = 0; each < pathTokens.length; each++) {
            query = query + 'cm:' + search.ISO9075Encode(pathTokens[each].trim()) + '/';
         }
         query = query + '/*"';
     } else {
         query = query + '/cm:documentLibrary//*"';
     }
	 
     query = query + ' AND (TYPE:"'+inputNodetype+'")';

    //Append additionalQuery query if any
    if ( !! additionalQuery) { //if not null then append
        query = query + ' AND ' + additionalQuery;
    }
    return query;
}


Sample Output:
DEBUG - Executing SearchQuery: PATH:"/app:company_home/st:sites/cm:test/cm:documentLibrary//*" AND (TYPE:"cm:content")
DEBUG - Total nodes found: 17
DEBUG - NodeName: xyz.jpeg | NodeType: {http://www.alfresco.org/model/content/1.0}content
DEBUG - Changing type for node: 'xyz.jpeg'
DEBUG - Change type to: '{http://www.github.com/abhinavmishra14/model/demo/1.0}whitePaper' has been completed.
DEBUG - NodeName: abc.jpg | NodeType: {http://www.alfresco.org/model/content/1.0}content
DEBUG - Changing type for node: 'abc.jpg'
DEBUG - Change type to: '{http://www.github.com/abhinavmishra14/model/demo/1.0}whitePaper' has been completed.
DEBUG - NodeName: download.png | NodeType: {http://www.alfresco.org/model/content/1.0}content
DEBUG - Changing type for node: 'download.png'
DEBUG - Change type to: '{http://www.github.com/abhinavmishra14/model/demo/1.0}whitePaper' has been completed.
















No comments:

Post a Comment

Thanks for your comments/Suggestions.