Friday, July 24, 2020

Change ACS 6.x/7.x, Share 6.x/7.x, Proxy (nginx), Solr6 and DB (postgres) ports using docker-compose.yml and DockerFile


It is a very common requirement to use different set of available port (s) as per company policy rather than using default port (s) for the applications.


Background:


Before container based environments, we had to follow below given steps in order to change the ports (these points still applies to an environment setup via distribution package):

    • Update default connector ports 8080, 8443, 8009 and 8005 to required ports.
    • Sometimes we use JPDA_ADDRESS for remote debug which is default '8000' in $ALFRESCO_INSTALL_DIR/tomcat/bin/catalina.sh. If we use remote debug, them update to required port as needed.
  • Update the required 'alfresco' and 'share' ports in $ALFRESCO_INSTALL_DIR/tomcat/shared/classes/alfresco-global.properties
  • Update the required 'alfresco' ports in $ALFRESCO_INSTALL_DIR/tomcat/shared/classes/alfresco/web-extension/share-config-custom.xml for remote configuration (<config evaluator="string-compare" condition="Remote">).
        • http://localhost:{REQUIRED_PORT}/alfresco/s --> DEFAULT: 8080
      • Update 'alfresco' endpoint url: 
        • http://localhost:{REQUIRED_PORT}/alfresco/s --> DEFAULT: 8080
      • Update 'alfresco-feed' endpoint url: 
        • http://localhost:{REQUIRED_PORT}/alfresco/s --> DEFAULT: 8080
      • Update 'alfresco-api' endpoint url: 
        • http://localhost:{REQUIRED_PORT}/alfresco/api --> DEFAULT: 8080
  • Update the required 'alfresco' port in 'solrcore.properties' file.
    • Find the 'alfresco.port' property in solrcore.properties file and update:
      • alfresco.port=<requiredPort>, default : 8080
    • For SOLR4, we used below paths:
      • $ALFRESCO_INSTALL_DIR/solr4/workspace-SpacesStore/conf/solrcore.properties
      • $ALFRESCO_INSTALL_DIR/solr4/archive-SpacesStore/conf/solrcore.properties
      • $SOLR_HOME/solrhome/alfresco/conf/solrcore.properties
      • $SOLR_HOME/solrhome/archive/conf/solrcore.properties



All the above given steps will be almost same for ACS 6.x as well if you are using standalone installation and not managing the services, images and containers via docker based deployment. 

When using docker based deployment, we use docker-compose.yml file to configure all the services which will be used as a base for launching the corresponding containers. We configure all the required ports (host and container ports) in the docker-compose.yml file and expose any ports if required either via docker-compose.yml or DockerFile.

It is possible to change the host ports via docker-compose.yml file but default ports (container ports) which are exposed within docker images (specially connector ports in tomcat which is shipped with acs and share images) can't be changed via docker-compose.yml alone. We have to take help of DockerFile, which can be used to update required ports at the time of build process.

Similarly, if you are using proxy (nginx) then 'ngnix.conf' configuration also needs an update to reference the required ports. By default ngnix will try to forward all requests to '8080' which is default port for acs and share.

It will be like re-builing the original images (acs, share, proxy etc. images) with updated ports and containers will be launced using the updated images. 

For some of the servives such as 'postgres', you can change the default port directly from docker-compose.yml as it gets access to command line, it is like executing 'postgres -p 5433' via command line. 
We can simply pass the command line param '-p <requiredPort>' or use 'expose' option in docker-compose.yml in the 'postgres' service definition.

For 'solr6 (alfresco-search-service)', we can either update the startup script or update the shared.properties via DockerFile or add SOLR_PORT environment variable in docker-compose.yml. This env variable will be used by jetty server to start service on required port. 
Additionally, you can also pass Jvm param using JAVA_OPTS, e.g. -Djetty.port=9999

Change Alfresco, Share, Nginx (Proxy), Solr and Database (postgres) ports with help of DockerFile and docker-compose.yml:


Considering the aforementioned steps for changing the ports, we need to follow the same  approach for docker based deployment as well but with help of docker-compose.yml and DockerFile.

I will be using port '7080' instead of '8080' for acs, share and proxy. I will also update the tomcat connector ports to 7005, 7009 and 7443. I will use '5555' instead of '5432' for postgres and '9999' instead of '8983' for solr6.


Here are default ports:

Service

Default Ports

Note

Tomcat connector ports

8005, 8080, 8443, 8009

Default within tomcat shipped with alfresco and share images.

alfresco

8080

 

share

8080

 

proxy

80, 8080 -> 8080

Default port on proxy(nginx) is 80, where port 8080 is exposed for providing access to alfresco and share. nginx forwards requests on 8080 (host port) to alfresco’s and share’s port 8080.

We can change the host port to any other port as well easily. E.g. 81 -> 8080 (Request will come on port 81 which nginx will forward to 8080)

postgres

5432

 

solr6

8083 -> 8983

8083 is host port and 8983 is container port. Alfresco uses 8983 to communicate with solr6. To access solr admin, administrators use 8083

 

Access via browser: http://localhost:8083/

 

transform-core-aio

8090-> 8090

Both host and container ports are same here.

Alfresco uses 8090 to communicate with transformation services.

We can use the port 8090 to access the transformation services via browser.

 

Access via browser: http://localhost:8090/

 

activemq

8161 -> 8161 # Web Console

Both host and container ports are same here.

port 8161 can be used for accessing the ‘Web Console’ via browser and alfresco would use the same port to communicate with activemq.

 

Access WebConsole via browser: http://localhost:8161/

 


The steps we are going to follow, are applicable to ACS 6.x , ACS7.1 and ACS 7.2

This post has been updated to match the latest ACS version (ACS 7.3) as well. 

Let's create some directories for keeping the DockerFile and required configs which will be used for re-building the updated images from OOTB images.

  • Create a directory 'configs-to-override' in the same directory where you have kept your 'docker-compose.yml' file.
  • Under 'configs-to-override' directory, create following directories:
    • Create 'alfresco' directory --> It will be used to keep 'DockerFile' for acs image
      • Create an empty 'DockerFile' file which we will use to put build instructions for 'alfresco' service
    • Create 'share' directory --> It will be used to keep 'DockerFile' for share image
      • Create an empty 'DockerFile' file which we will use to put build instructions for 'share' service
    • Create 'proxy' directory --> It will be used to keep 'DockerFile' and 'nginx.conf' file for nginx image
      • Create an empty 'DockerFile' file which we will use to put build instructions for 'proxy' service
      • Create an empty 'nginx.conf' file which we will use to put proxy configuration for services

Monday, June 1, 2020

Unlock a node in Alfresco which could not be unlocked


I had an issue recently when i was trying to edit a word document. I deleted the working copy node somehow. I have no clues how it happend. But this word document node was locked foreever. Cancel checkout action was always failing. I was not able to cancel checkout and unlock the node.

While i was investigating the issue, i looked for the nodeRef of the word document via node browser. I found that it had cm:checkedOut aspect (because i checkedOut for edit oboviously).

I thought removing the aspect and unlocking will solve my problem. I tried to remove cm:checkedOut aspect from the node via repository js webscript but it failed. 

In my further investigation i reviewed, org.alfresco.service.cmr.coci.CheckOutCheckInService.cancelCheckout(NodeRef) method defintion in CheckOutCheckInService class:

public NodeRef cancelCheckout(NodeRef workingCopyNodeRef);

It indicated that, it expects workingCopyNode in order to cancel the checkout.

To unlock the node i have to somehow remove the cm:checkout aspect and then unlock the node. 

When i investigated more in logs (enabled the debug log on org.alfresco.repo.coci package), i saw that on click of Cancel Checkout, a backend behavior (org.alfresco.repo.coci.CheckOutCheckInServicePolicies.BeforeCancelCheckOut) was stopping the process. The below given method seemd to be validating working copy node by firing org.alfresco.repo.coci.CheckOutCheckInServicePolicies.BeforeCancelCheckOut.beforeCancelCheckOut(NodeRef) behavior.

org.alfresco.repo.coci.CheckOutCheckInServiceImpl.invokeBeforeCancelCheckOut(NodeRef)

The above validation was happening before the unlock (org.alfresco.service.cmr.lock.LockService.unlock(NodeRef, boolean, boolean)) method call. 

So i decided to disable the behaviors and then unlock the node followed by aspect removal. Here is the fix:

function main () { 
//using the string nodeRef of the word document which was locked. var nodeRef = search.findNode("workspace://SpacesStore/7c093a3d-c16e-416d-8087-9ccd93aa2a06"); var webContext = Packages.org.springframework.web.context.ContextLoader.getCurrentWebApplicationContext(); var behaviourFilter = webContext.getBean("policyBehaviourFilter", org.alfresco.repo.policy.BehaviourFilter);  
// Disable all behaviors, so we can remove the aspect without any validations behaviourFilter.disableAllBehaviours(); // Remove cm:checkedOut aspect and try to unlock the node which was not getting unlocked due to behavior validations nodeRef.removeAspect("cm:checkedOut"); nodeRef.unlock(); // Enable all the behaviors behaviourFilter.enableAllBehaviours(); 
} main();

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.

Wednesday, January 22, 2020

Display more than 50 items per page in Alfresco share document library and trashcan


There is a little bit difference in paginator component for document library and trashcan. Both can be extended to set the value as per need. Paginator in document library provides widget option for pageSize which can be extended just by updating the pageSize option in documentlist-v2.get.js webscript. Whereas pageSize is setup directly in a yui widget (usertrashcan.js) file and it doesn't provide options to update it via webscript.

Let's say we want to display 100 items per page.

Follow these steps to update the pageSize for document library and trashcan:

1- Add a new Surf Extension Modules file called custom-share-extension-modules.xml to the <yourShareProject>/src/main/resources/alfresco/web-extension/site-data/extensions directory

<extension>
 <modules>

  <module>

   <id>Custom_Share_Module_Extension</id>

   <version>1.0</version>

   <auto-deploy>true</auto-deploy>

   <customizations>

    <!-- Extend the document library to display 100 files per page. -->

    <customization>

     <targetPackageRoot>org.alfresco.components.documentlibrary</targetPackageRoot>

     <sourcePackageRoot>com.demo.components.documentlibrary</sourcePackageRoot>

    </customization>



    <!-- Extend the trashcan to display 100 files per page. -->

    <customization>

     <targetPackageRoot>org.alfresco.components</targetPackageRoot>

     <sourcePackageRoot>com.demo.components</sourcePackageRoot>

    </customization>

   </customizations>

  </module>

 </modules>

</extension>

Create folder structure as: com/demo/components/documentlibrary under extensions folder

2- Create a file "documentlist-v2.get.js" under  com/demo/components/documentlibrary folder as setup above (use surf bug to identify which file to extend for which component. Look at this document here: https://docs.alfresco.com/5.2/concepts/dev-extensions-share-surfbug.html)

3- Add following code to the documentlist-v2.get.js file, see highlighted line:

<import resource="classpath:/alfresco/site-webscripts/org/alfresco/components/documentlibrary/include/toolbar.lib.js">
<import resource="classpath:/alfresco/site-webscripts/org/alfresco/components/upload/uploadable.lib.js">
<import resource="classpath:/alfresco/site-webscripts/org/alfresco/components/documentlibrary/include/documentlist.lib.js">

doclibCommon();

function widgets()
{
  //Show 100 files per page. Default value was 50
  model.widgets[1].options.pageSize = 100;
}

widgets();


This controller will be processed after the out-of-the-box documentlist-v2 controller.

4- By following the same above steps, find the out-of-the-box user-trashcan webscript files (see the list of files below which needs to be copied) and copy them under com/demo/components/ folder

user-trashcan.get.html.ftl
user-trashcan.get.js

Here we are going to extend the YUI widget itself by overriding the pageSize value in usertrashcan.js

5- Create a "usertrashcan-custom.js" file under <yourProject>/src/main/resources/META-INF/components/profile/ folder.

6- Edit the usertrashcan-custom.js file with following code, notice the highlighted value of pageSize.

// Declare namespace
if (typeof Custom == undefined || !Custom) { var Custom = {}; }
if (!Custom.Alfresco) { Custom.Alfresco = {}; }
if (!Custom.Alfresco.UserTrashcan) { Custom.Alfresco.UserTrashcan = {}; }

/**
 * Custom User Trashcan component.
 * 
 * @namespace Alfresco
 * @class Alfresco.UserTrashcan
 */
(function()
{
   /**
    * YUI Library aliases
    */
   var Dom = YAHOO.util.Dom,
       Event = YAHOO.util.Event;
   
   /**
    * Alfresco Slingshot aliases
    */
   var $html = Alfresco.util.encodeHTML;
   
   /**
    * Custom User Trashcan constructor.
    * 
    * @param {String} htmlId The HTML id of the parent element
    * @return {Custom.Alfresco.UserTrashcan} The new UserTrashcan instance
    * @constructor
    */
   Custom.Alfresco.UserTrashcan = function(htmlId)
   {
      Custom.Alfresco.UserTrashcan.superclass.constructor.call(this, "Custom.Alfresco.UserTrashcan", htmlId, ["button", "container", "datasource", "datatable", "paginator"]);
      this.searchText = "";
      return this;
   }
   
   YAHOO.extend(Custom.Alfresco.UserTrashcan, Alfresco.component.Base,
   {
      searchText: null,
      //Show 100 files per page.
      pageSize: 100,
      skipCount: 0,
      
      /**
       * Fired by YUI when parent element is available for scripting.
       * Component initialisation, including instantiation of YUI widgets and event listener binding.
       *
       * @method onReady
       */
      onReady: function UT_onReady()
      {
         // Reference to self used by inline functions
         var me = this;
         
         // Buttons and menus
         this.widgets.empty = Alfresco.util.createYUIButton(this, "empty-button", this.onEmpty);
         this.widgets.search = Alfresco.util.createYUIButton(this, "search-button", this.onSearch);
         this.widgets.clear = Alfresco.util.createYUIButton(this, "clear-button", this.onClear);
         this.widgets.pageLess = Alfresco.util.createYUIButton(this, "paginator-less-button", this.onPageLess);
         this.widgets.pageMore = Alfresco.util.createYUIButton(this, "paginator-more-button", this.onPageMore);
         this.widgets.actionMenu = Alfresco.util.createYUIButton(this, "selected", this.onActionItemClick,
         {
            disabled: true,
            type: "menu",
            menu: "selectedItems-menu"
         });
         this.widgets.selectMenu = Alfresco.util.createYUIButton(this, "select-button", this.onSelectItemClick,
         {
            type: "menu",
            menu: "selectItems-menu"
         });
         
         // Enter key press handler for search text field
         var me = this;
         Dom.get(this.id + "-search-text").onkeypress = function(e)
         {
            if (e.keyCode === YAHOO.util.KeyListener.KEY.ENTER)
            {
               me.performSearch();
            }
         };
         
         // Configure datatable
         var url = Alfresco.constants.PROXY_URI + "api/archive/workspace/SpacesStore";
         this.widgets.dataTable = new Alfresco.util.DataTable(
         {
            dataTable:
            {
               container: this.id + "-datalist",
               columnDefinitions:
               [
                  { key: "select", sortable: false, formatter: this.bind(this.renderCellSelect), width: 16 },
                  { key: "thumbnail", sortable: false, formatter: this.bind(this.renderCellIcon), width: 32 },
                  { key: "description", sortable: false, formatter: this.bind(this.renderCellDescription) },
                  { key: "actions", sortable: false, formatter: this.bind(this.renderCellActions), width: 250 }
               ]
            },
            dataSource:
            {
               url: url,
               initialParameters: "maxItems=" + (this.pageSize + 1),
               config:
               {
                  responseSchema:
                  {
                     resultsList: "data.deletedNodes"
                  },
                  doBeforeParseData: function _doBeforeParseData(oRequest, oResponse)
                  {
                     // process the paging meta data to correctly set paginator button enabled state
                     me.widgets.pageLess.set("disabled", ((me.skipCount = oResponse.paging.skipCount) === 0));
                     if (oResponse.paging.totalItems > me.pageSize)
                     {
                        // remove the last item as it's only for us to evaluate the "more" button state
                        oResponse.data.deletedNodes.pop();
                        me.widgets.pageMore.set("disabled", false);
                     }
                     else
                     {
                        me.widgets.pageMore.set("disabled", true);
                     }
                     return oResponse;
                  }
               }
            }
         });
      },
      
      /**
       * DataTable Cell Renderers
       */
      
      /**
       * Select checkbox custom datacell formatter
       *
       * @method UserTrashcan_renderCellSelect
       */
      renderCellSelect: function UserTrashcan_renderCellSelect(elCell, oRecord, oColumn, oData)
      {
         Dom.setStyle(elCell.parentNode, "width", oColumn.width + "px");
         
         var me = this;
         elCell.innerHTML = '<input id="checkbox-' + oRecord.getId() + '" type="checkbox" value="'+ oRecord.getData("nodeRef") + '">';
         elCell.firstChild.onclick = function() {
            me._updateSelectedItemsMenu();
         };
      },
      
      /**
       * File/Folder icon custom datacell formatter
       *
       * @method UserTrashcan_renderCellIcon
       */
      renderCellIcon: function UserTrashcan_renderCellIcon(elCell, oRecord, oColumn, oData)
      {
         Dom.setStyle(elCell.parentNode, "width", oColumn.width + "px");
         
         var name = oRecord.getData("name"),
             type = oRecord.getData("nodeType");
         
         elCell.innerHTML = '<span class="icon32"><img src="' + Alfresco.constants.URL_RESCONTEXT + 'components/images/filetypes/' + Alfresco.util.getFileIcon(name, oRecord.getData("isContentType") ? "cm:content" : type) + '" alt="' + $html(name) + '" /></span>';
      },
      
      /**
       * Description metadata custom datacell formatter
       * 
       * @method UserTrashcan_renderCellDescription
       */
      renderCellDescription: function UserTrashcan_renderCellDescription(elCell, oRecord, oColumn, oData)
      {
         var fullName = oRecord.getData("firstName") + " " + oRecord.getData("lastName");
         var viewUrl = Alfresco.constants.PROXY_URI_RELATIVE + "api/node/content/" + oRecord.getData("nodeRef").replace(":/", "") + "/" + encodeURIComponent(oRecord.getData("name"));
         var profileLink = '<a href="' + Alfresco.constants.URL_PAGECONTEXT + 'user/' + encodeURI(oRecord.getData("archivedBy")) + '/profile">' + $html(fullName) + '</a>';
         var meta = this.msg("message.metadata", Alfresco.util.formatDate(Alfresco.util.fromISO8601(oRecord.getData("archivedDate"))), profileLink);
         var item = oRecord.getData("isContentType") ? '<a href="' + viewUrl + '?a=true">' + $html(oRecord.getData("name")) + '</a>' : $html(oRecord.getData("name"));
         
         var desc = '<div class="name">' + item + '</div><div class="desc">' + meta + '</div>'
                  + '<div class="desc">' + $html(oRecord.getData("displayPath")) + '</div>';
         
         elCell.innerHTML = desc;
      },
      
      /**
       * Actions custom datacell formatter
       *
       * @method UserTrashcan_renderCellActions
       */
      renderCellActions: function UserTrashcan_renderCellActions(elCell, oRecord, oColumn, oData)
      {
         Dom.setStyle(elCell.parentNode, "vertical-align", "middle");
         Dom.setStyle(elCell.parentNode, "text-align", "right");
         
         var nodeRef = oRecord.getData("nodeRef"),
             nodeType = oRecord.getData("nodeType"),
             nodeName = oRecord.getData("name");
         
         this._createActionButton(
            elCell, nodeRef.split("/")[3], "button.recover",
            function(event, obj) 
            {
               if (!this.isRecoverEnabled())
               {
                  return;
               }
               
               this._disableRecover();
               
               this.restoringPopup = Alfresco.util.PopupManager.displayMessage(
               {
                  displayTime: 0,
                  effect: null,
                  spanClass: "wait",
                  text: me.msg("message.recover.inprogress")
               });

               // make ajax call to Recover the item
               Alfresco.util.Ajax.jsonPost(
               {
                  // TODO: call a Share service to complete the delete operation - this allows additional
                  // behaviour hooks to purge the related view caches to ensure site dashboard etc. is usable again
                  url: Alfresco.constants.URL_SERVICECONTEXT + "modules/recover-node",
                  dataObj: {
                     name: obj.name,
                     nodeType: obj.nodeType,
                     nodeRef: obj.nodeRef.replace(":/","")
                  },
                  successCallback:
                  {
                     fn: this._onRecoverSuccess,
                     obj: obj,
                     scope: this
                  },
                  failureCallback: 
                  {
                     fn: this._onRecoverFailure,
                     obj: obj,
                     scope: this
                  }
               });
            },
            {
               nodeRef: nodeRef,
               nodeType: nodeType,
               name: nodeName
            }
         );
         var me = this;
         this._createActionButton(
            elCell, nodeRef.split("/")[3], "button.delete",
            function(event, obj) 
            {
               // confirm this brutal operation with the user
               Alfresco.util.PopupManager.displayPrompt(
               {
                  title: me.msg("button.delete"),
                  text: me.msg("message.delete.confirm"),
                  buttons: [
                     {
                        text: me.msg("button.ok"),
                        handler: function()
                        {
                           this.destroy();
                           // make ajax call to Delete the item
                           Alfresco.util.Ajax.request(
                           {
                              url: Alfresco.constants.PROXY_URI + "api/archive/" + obj.nodeRef.replace(":/",""),
                              method: "DELETE",
                              successCallback:
                              {
                                 fn: me._onDeleteSuccess,
                                 obj: obj,
                                 scope: me
                              },
                              failureMessage: me.msg("message.delete.failure", nodeName)
                           });
                        }
                     },
                     {
                        text: me.msg("button.cancel"),
                        handler: function()
                        {
                           this.destroy();
                        },
                        isDefault: true
                     }
                  ]
               });
            },
            {
               nodeRef: nodeRef,
               name: nodeName
            }
         );
      },
      
      /**
       * Enables recover actions
       */
      _enableRecover: function UT_enableRecover()
      {
         this._isRecoverEnabled = true;
      },
      
      /**
       * Disables recover actions
       */
      _disableRecover: function UT_disableRecover()
      {
         this._isRecoverEnabled = false;
      },
      
      /**
       * Indicates is recover action is enabled
       */
      isRecoverEnabled: function UT_isRecoverEnabled()
      {
         return this._isRecoverEnabled !== false;
      },
      
      /**
       * Callback handler used when a deleted item was recovered
       * 
       * @method _onRecoverSuccess
       * @param response {object}
       * @param obj {object}
       */
      _onRecoverSuccess: function UT__onRecoverSuccess(response, obj)
      {
         this.restoringPopup.destroy();
         this._enableRecover();
         
         Alfresco.util.PopupManager.displayMessage(
         {
            text: this.msg("message.recover.success", obj.name)
         });
         
         this.refreshDataTable();
      },
      
      /**
       * Callback handler used when a deleted item wasn't recovered
       * 
       * @method _onRecoverSuccess
       * @param response {object}
       * @param obj {object}
       */
      _onRecoverFailure: function UT__onRecoverFailure(response, obj)
      {
         this.restoringPopup.destroy();
         this._enableRecover();
         
         Alfresco.util.PopupManager.displayMessage(
         {
            text: this.msg("message.recover.failure", obj.name)
         });
         
         this.refreshDataTable();
      },
      
      /**
       * Callback handler used when a deleted item was purged
       * 
       * @method _onDeleteSuccess
       * @param response {object}
       * @param obj {object}
       */
      _onDeleteSuccess: function UT__onDeleteSuccess(response, obj)
      {
         Alfresco.util.PopupManager.displayMessage(
         {
            text: this.msg("message.delete.success", obj.name)
         });
         
         this.refreshDataTable();
      },

      /**
       * YUI WIDGET EVENT HANDLERS
       */
       
      /**
       * Selected items action menu event handler
       * @method onActionItemClick.
       * @param sType, aArgs, p_obj
       */
      onActionItemClick: function UT_onActionItemClick(sType, aArgs, p_obj)
      {
         var items = [],
             dt = this.widgets.dataTable.getDataTable(),
             rows = dt.getTbodyEl().rows;
         for (var i = 0; i < rows.length; i++)
         {
            if (rows[i].cells[0].getElementsByTagName('input')[0].checked)
            {
               var data = dt.getRecord(i);
               if (data)
               {
                  items.push(data);
               }
            }
         }
         
         var me = this;
         switch (aArgs[1]._oAnchor.className.split(" ")[0])
         {
            case "delete-item":
               // confirm this brutal operation with the user
               Alfresco.util.PopupManager.displayPrompt(
               {
                  title: me.msg("button.delete"),
                  text: me.msg("message.delete.confirm"),
                  buttons: [
                     {
                        text: me.msg("button.ok"),
                        handler: function()
                        {
                           this.destroy();
                           var failed = [],
                               total = 0;
                           for (var i=0; i<items.length; i++)
                           {
                              // make ajax calls to Delete the items
                              Alfresco.util.Ajax.request(
                              {
                                 url: Alfresco.constants.PROXY_URI + "api/archive/" + items[i].getData("nodeRef").replace(":/",""),
                                 method: "DELETE",
                                 failureCallback: {
                                    fn: function() {
                                       failed.push(items[i].getData("name"));
                                       total++;
                                    },
                                    obj: items[i],
                                    scope: me
                                 },
                                 successCallback: {
                                    fn: function() {
                                       total++;
                                    },
                                    obj: items[i],
                                    scope: me
                                 }
                              });
                           }
                           var completeFn = function() {
                              if (total === items.length)
                              {
                                 Alfresco.util.PopupManager.displayPrompt(
                                 {
                                    title: me.msg("message.delete.report"),
                                    text: me.msg("message.delete.report-info", (items.length-failed.length), failed.length)
                                 });
                                 me.refreshDataTable();
                              }
                              else
                              {
                                 setTimeout(completeFn, 500);
                              }
                           };
                           setTimeout(completeFn, 500);
                        }
                     },
                     {
                        text: me.msg("button.cancel"),
                        handler: function()
                        {
                           this.destroy();
                        },
                        isDefault: true
                     }
                  ]
               });
               break;
            case "recover-item":
               var failed = [],
                   total = 0;
               
               if (!me.isRecoverEnabled())
               {
                  return;
               }
               
               me._disableRecover();
               
               me.restoringPopup = Alfresco.util.PopupManager.displayMessage(
               {
                  displayTime: 0,
                  effect: null,
                  spanClass: "wait",
                  text: me.msg("message.recover.inprogress")
               });

               for (var i=0; i<items.length; i++)
               {
                  var index = i;
                  // make ajax call to Recover the item
                  Alfresco.util.Ajax.request(
                  {
                     url: Alfresco.constants.PROXY_URI + "api/archive/" + items[i].getData("nodeRef").replace(":/",""),
                     method: "PUT",
                     failureCallback: {
                        fn: function() {
                           failed.push(items[index].getData("name"));
                           total++;
                        },
                        obj: items[i],
                        scope: me
                     },
                     successCallback: {
                        fn: function() {
                           total++;
                        },
                        obj: items[i],
                        scope: me
                     }
                  });
               }
               var completeFn = function() {
                  if (total === items.length)
                  {
                     me.restoringPopup.destroy();
                     me._enableRecover();
                     
                     Alfresco.util.PopupManager.displayPrompt(
                     {
                        title: me.msg("message.recover.report"),
                        text: me.msg("message.recover.report-info", (items.length-failed.length), failed.length)
                     });
                     me.refreshDataTable();
                  }
                  else
                  {
                     setTimeout(completeFn, 250);
                  }
               };
               setTimeout(completeFn, 250);
               break;
         }
      },
      
      /**
       * Select items menu item event handler
       * @method onSelectItemClick.
       * @param sType, aArgs, p_obj
       */
      onSelectItemClick: function UT_onSelectItemClick(sType, aArgs, p_obj)
      {
         switch (aArgs[1]._oAnchor.className.split(" ")[0])
         {
            case "select-all":
               this._selectAll();
               break;
            case "select-invert":
               this._invertSelection();
               break;
            case "select-none":
               this._deselectAll();
               break;
         }
      },
      
      /**
       * Select all items.
       * @method _selectAll
       */
      _selectAll: function UT__selectAll()
      {
         var rows = this.widgets.dataTable.getDataTable().getTbodyEl().rows;
         for (var i = 0; i < rows.length; i++)
         {
            rows[i].cells[0].getElementsByTagName('input')[0].checked = true;
         }
         this._updateSelectedItemsMenu();
      },
      
      /**
       * Deselect all items.
       * @method _deselectAll
       */
      _deselectAll: function UT__deselectAll()
      {
         var rows = this.widgets.dataTable.getDataTable().getTbodyEl().rows;
         for (var i = 0; i < rows.length; i++)
         {
            rows[i].cells[0].getElementsByTagName('input')[0].checked = false;
         }
         this._updateSelectedItemsMenu();
      },
      
      /**
       * Invert selection of items.
       * @method _invertSelection
       */
      _invertSelection: function UT__invertSelection()
      {
         var rows = this.widgets.dataTable.getDataTable().getTbodyEl().rows;
         for (var i = 0; i < rows.length; i++)
         {
            var check = rows[i].cells[0].getElementsByTagName('input')[0];
            check.checked = !check.checked;
         }
         this._updateSelectedItemsMenu();
      },
      
      /**
       * Update the disabled status of the multi-select action menu based on the state of the item checkboxes
       * @method _updateSelectedItemsMenu
       */
      _updateSelectedItemsMenu: function UT__updateSelectedItemsMenu()
      {
         this.widgets.actionMenu.set("disabled", true);
         var rows = this.widgets.dataTable.getDataTable().getTbodyEl().rows;
         for (var i = 0; i < rows.length; i++)
         {
            if (rows[i].cells[0].getElementsByTagName('input')[0].checked)
            {
               this.widgets.actionMenu.set("disabled", false);
               break;
            }
         }
      },
      
      /**
       * OnSearch button click handler
       * 
       * @method onSearch
       * @param e {object} DomEvent
       * @param p_obj {object} Object passed back from addListener method
       */
      onSearch: function UT_onSearch(e, p_obj)
      {
         this.performSearch();
      },
      
      /**
       * onClear button click handler
       * 
       * @method onClear
       * @param e {object} DomEvent
       * @param p_obj {object} Object passed back from addListener method
       */
      onClear: function UT_onClear(e, p_obj)
      {
         Dom.get(this.id + "-search-text").value = "";
         if (this.searchText.length !== 0)
         {
            this.searchText = "";
            this.refreshDataTable();
         }
      },
      
      /**
       * OnEmpty button click handler
       * 
       * @method onEmpty
       * @param e {object} DomEvent
       * @param p_obj {object} Object passed back from addListener method
       */
      onEmpty: function UT_onEmpty(e, p_obj)
      {
         var me = this;
         
         // confirm this brutal operation with the user
         Alfresco.util.PopupManager.displayPrompt(
         {
            title: me.msg("button.empty"),
            text: me.msg("message.empty.confirm"),
            buttons: [
            {
               text: me.msg("button.ok"),
               handler: function()
               {
                  this.destroy();
                  
                  // call api to remove all items from the trashcan
                  // use the progress animation as this operation may take a while
                  var progressPopup = Alfresco.util.PopupManager.displayMessage(
                  {
                     displayTime: 0,
                     effect: null,
                     text: me.msg("message.empty.inprogress")
                  });
                  
                  Alfresco.util.Ajax.request(
                  {
                     url: Alfresco.constants.PROXY_URI + "api/archive/workspace/SpacesStore",
                     method: "DELETE",
                     successCallback:
                     {
                        fn: function success(data)
                        {
                            progressPopup.destroy();
                            me.refreshDataTable();
                        }
                     },
                     failureCallback:
                     {
                        fn: function failure(data)
                        {
                           progressPopup.destroy();
                           
                           Alfresco.util.PopupManager.displayPrompt(
                           {
                              text: me.msg("message.recover.failure")
                           });
                        }
                     }
                  });
               }
            },
            {
               text: me.msg("button.cancel"),
               handler: function()
               {
                  this.destroy();
               },
               isDefault: true
            }]
         });

         var elements = Dom.getElementsByClassName('yui-button', 'span', 'prompt');
         Dom.addClass(elements[0], 'alf-primary-button');
      },
      
      /**
       * onPageLess button click handler
       * 
       * @method onPageLess
       * @param e {object} DomEvent
       * @param p_obj {object} Object passed back from addListener method
       */
      onPageLess: function UT_onPageLess(e, p_obj)
      {
         if (this.skipCount > 0)
         {
            this.skipCount -= this.pageSize;
         }
         this.refreshDataTable();
      },
      
      /**
       * onPageMore button click handler
       * 
       * @method onPageMore
       * @param e {object} DomEvent
       * @param p_obj {object} Object passed back from addListener method
       */
      onPageMore: function UT_onPageMore(e, p_obj)
      {
         this.skipCount += this.pageSize;
         this.refreshDataTable();
      },
      
      /**
       * Create a generic YUI action button hooked into the appropriate parent element
       * 
       * @method createActionButton
       * @param el {object} Parent element to add button
       * @param id {string} Button ID
       * @param labelid {string} Button label message ID
       * @param action {function} Action event function
       * @param obj {object} Action event parameter object
       * @private
       */
      _createActionButton: function UT_createActionButton(el, id, labelId, action, obj)
      {
         var me = this;
         var span = document.createElement("span");
         span.id = me.id + id;
         var btn = new YAHOO.widget.Button(
         {
             container: me.id + id
         });
         btn.set("label", me.msg(labelId));
         btn.set("onclick",
         {
            fn: action,
            obj: obj,
            scope: me
         });
         el.appendChild(span);
      },
      
      /**
       * Update the current search terms from the search text field and perform a search.
       * 
       * @method performSearch
       */
      performSearch: function UT_performSearch()
      {
         // MNT-12799: resetting 'skipCount' to show search results from the first page
         this.skipCount = 0;

         var searchText = YAHOO.lang.trim(Dom.get(this.id + "-search-text").value);
         if (searchText.length !== 0)
         {
            this.searchText = searchText;
            this.refreshDataTable();
         }
      },
      
      /**
       * Refresh the list after an action has occured
       * 
       * @method refreshDataTable
       */
      refreshDataTable: function UT_refreshDataTable()
      {
         // we alway ask for an extra item to see if there are more for the next page
         var params = "maxItems=" + (this.pageSize + 1) + "&skipCount=" + this.skipCount;
         if (this.searchText.length !== 0)
         {
            var search = this.searchText;
            if (search.match("\\*") != "*")
            {
               search += "*";
            }
            params += "&nf=" + encodeURIComponent(search);
         }
         this.widgets.dataTable.loadDataTable(params);
      }
   });
})();



7- Edit the user-trashcan.get.html.ftl file and update the reference to usertrashcan.js to usertrashcan-custom.js:

<@markup id="custom-js" target="js" action="replace">
   <#-- JavaScript Dependencies -->
   <#-- Overridden usertrashcan.js to increase the pageSize from 50 to 100 -->
   <@script src="${url.context}/res/components/profile/usertrashcan-custom.js" group="profile"/>
</@>

8- Edit the "user-trashcan.get.js" file and add following code. Here we are overriding the out-of-the-box UserTrashcan widget:

/**
 * Custom User Profile Component - User Trashcan GET method
 */
//Find the default UserTrashcan widget and replace it with the custom widget
for (var i=0; i<model.widgets.length; i++) {
  if (model.widgets[i].id == "UserTrashcan") {
      model.widgets[i].name = "Custom.Alfresco.UserTrashcan";
  }
}


9- Now, restart the server. You should see try uploading 101 files in document library, you should see that 100 files are on page 1 and next page would have 1 file. Now delete all these files and go to trashcan and you should notice the same behavior.

Note that, in document library you have option to go by page number but in trashcan you have to follow the << >> links.


To learn more on how to extend surf webscripts, read this document: 
https://docs.alfresco.com/6.2/concepts/dev-extensions-share-surf-extension-modules.html
https://docs.alfresco.com/6.2/tasks/dev-extensions-share-tutorials-js-customize.html
https://docs.alfresco.com/6.2/tasks/dev-extensions-share-tutorials-customizing-widget-instantiation.html