Friday, December 4, 2020

Setup ACS62-ga and ASS1.4 using distribution package step by step Part-1

 

ACS6.x is mainly based on containers which makes use of popular container platform Docker

Many of us are not familiar with containerization setup and may not be ready to use it yet. In previous versions we had option to use installers and it used to setup everything seamlessly. With ACS6.x we don’t have installer option available and it is expected to leverage the use of containerization technologies.

But there are ways to setup ACS6.x in the same ways as we used to do for previous versions i.e. using distribution package provided by Alfresco.

The purpose of this post is to document all the steps at one place when setting up ACS6.x manually using distribution package.

ACS 6.2.0-ga is the latest stable version available and we will use the same version here.


What we need before we start doing setup?

  1. ACS-6.2.0-ga package (alfresco-content-services-community-distribution-6.2.0-ga)
  2. ASS-1.4.0 package (alfresco-search-services-1.4.0)
  3. Java: Oracle jdk-11.0.1 or later/Open JDK 11.0.1 or later
  4. Tomcat: Tomcat 8.5.43
  5. ActiveMQ: ActiveMQ v5.15.8 (Optional). Mandatory only when using transformation services
  6. DB: PostgreSQL 11.4
  7. ImageMagick: ImageMagick v7.0.10
  8. Libreoffice: LibreOffice v6.3.5
Checkout this documentation for additional details on Supported Platforms

Platform:

  • Windows 10 x64
Type of deployment:

  • ACS, Share and SOLR6 on same machine
  • Setup Without SSL

For Linux based installation steps visit this post: 

Setup ACS62-ga and ASS1.4 using distribution package part2

Interested in ACS7.x? checkout this post:



Let’s start gathering all the required prerequisites listed above.



Visit Alfresco community download page and download latest stable versions of ACS and ASS:

  • Download ACS 6.2.0-ga:
https://download.alfresco.com/cloudfront/release/community/201911-GA-build-368/alfresco-content-services-community-distribution-6.2.0-ga.zip
  • Download ASS 1.4.0:
https://download.alfresco.com/cloudfront/release/community/SearchServices/1.4.0/alfresco-search-services-1.4.0.zip

Note: If you are planning to setup ACS 6.2.1/6.2.2 (Enterprise versions) and ASS 2.0, then download the appropriate distribution packages from Alfresco Support portal. All the steps outlined below will remain same. We are using community version of ACS 6.2  for this post hence we have to use ASS 1.4 as stated in the Supported platforms here


Download and Install Oracle JDK 11.0.4:

https://www.oracle.com/java/technologies/javase/jdk11-archive-downloads.html

https://www.oracle.com/webapps/redirect/signon?nexturl=https://download.oracle.com/otn/java/jdk/11.0.4+10/cf1bbcbf431a474eb9fc550051f4ee78/jdk-11.0.4_windows-x64_bin.zip

Note: Make sure you set the JAVA_HOME environment variable (on windows). It is the installation path of jdk. E.g. JAVA_HOME=C:\Program Files\Java\jdk-11.0.4

Download Tomcat 8.5.43 binary package:

https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.43/bin/apache-tomcat-8.5.43-windows-x64.zip

Note: Make sure ports 8005, 8080, 8443, AJP port 8009 are open and not in use already. These are default ports used for tomcat. If you have these ports already in use, make sure you change the ports accordingly in <TOMCAT_INSTALLATION>/conf/server.xml.

Download ActiveMQ v5.15.8 binary package, it will be used for transformation services down the line:

https://archive.apache.org/dist/activemq/5.15.8/apache-activemq-5.15.8-bin.zip

Download and Install PostgreSQL 11.4:

https://get.enterprisedb.com/postgresql/postgresql-11.4-1-windows-x64.exe

Alternatively you can also download the binary package and extract it. No installation needed. It is useful if you have trouble doing installation on Windows 10.

Download PostgreSQL 11 binary

Note: Make sure port 5432 is open and not already in use. Port 5432 is default for postgres to get db connection. If you have this port already in use, make sure you select a different port and use the same while configuring alfresco-global.properties.


Download ImageMagick v7.0.10:

https://imagemagick.org/download/binaries/ImageMagick-7.0.10-45-Q16-HDRI-x64-dll.exe

Above version is no longer available to download, use the below given alternatives.


Alternative options:

Download LibreOffice v6.3.5:

https://downloadarchive.documentfoundation.org/libreoffice/old/6.3.5.2/win/x86_64/LibreOffice_6.3.5.2_Win_x64.msi (installer)

Optional Alfresco module packages (amps)-Useful for admins/developers:

https://repo1.maven.org/maven2/org/orderofthebee/support-tools/support-tools-repo/1.1.0.0/support-tools-repo-1.1.0.0-amp.amp 

https://repo1.maven.org/maven2/org/orderofthebee/support-tools/support-tools-share/1.1.0.0/support-tools-share-1.1.0.0-amp.amp 

https://repo1.maven.org/maven2/de/fmaul/javascript-console-repo/0.6/javascript-console-repo-0.6.amp 

https://repo1.maven.org/maven2/de/fmaul/javascript-console-share/0.6/javascript-console-share-0.6.amp

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.