Issue
When a custom portlet is deprecated,it often remains in hundreds of pages across Liferay instances. Manually removing each instance and cleaning up the associated database entries is impractical. A method is needed to automate the complete removal of the portlet from all layouts (both widget pages and content pages) and purge its related data from the database.
Environment
-
Liferay DXP 7.3+
Resolution
Use a Groovy script to automate the removal process across your instance.
-
Navigate to Control Panel → Server Administration → Script.
-
Select Groovy as the language.
-
Copy and paste the script below into the script editor.
-
Configure the script variables at the top of the script:
-
targetPortletId: Set this to the full ID of the portlet you want to delete (e.g.,"com_my_company_web_MyPortlet"). -
dryRun: Set this totruefor the initial run. This will scan and report what would be changed without actually modifying any data. Review the output to ensure it targets the correct items. -
targetGroupId: (Optional) If you want to limit the script to a single site, set this to the site's Group ID. Leave it as0to scan all sites.
-
-
Click Execute and review the output in the "Result" section.
-
Once you have verified the output of the dry run, change
dryRuntofalseand execute the script again to perform the deletion.
Additional information
On Liferay portal version 7.4+, the database / orphaned data cleanup actions are much more refined and work for this purpose.
import com.liferay.fragment.processor.PortletRegistry
import com.liferay.portal.kernel.service.LayoutLocalServiceUtil
import com.liferay.portal.kernel.model.Layout
import com.liferay.portal.kernel.model.LayoutTypePortlet
import com.liferay.portal.kernel.json.JSONArray
import com.liferay.portal.kernel.json.JSONObject
import com.liferay.portal.kernel.json.JSONFactoryUtil
import com.liferay.layout.page.template.service.LayoutPageTemplateStructureLocalServiceUtil
import com.liferay.layout.page.template.service.LayoutPageTemplateStructureRelLocalServiceUtil
import com.liferay.layout.page.template.model.LayoutPageTemplateStructure
import com.liferay.layout.page.template.model.LayoutPageTemplateStructureRel
import com.liferay.fragment.service.FragmentEntryLinkLocalServiceUtil
import com.liferay.fragment.model.FragmentEntryLink
import com.liferay.registry.RegistryUtil
import com.liferay.registry.Registry
import com.liferay.registry.ServiceTracker
import com.liferay.portal.kernel.workflow.WorkflowConstants
import java.util.ArrayList
import java.util.HashSet
import java.util.List
import java.util.Set
import java.util.Map
import java.util.HashMap
import java.util.Iterator
String targetPortletId = ""
boolean dryRun = true
long targetGroupId = 0
Registry registry = RegistryUtil.getRegistry()
ServiceTracker<PortletRegistry, PortletRegistry> serviceTracker = registry.trackServices(PortletRegistry.class)
serviceTracker.open()
PortletRegistry portletRegistry = serviceTracker.getService()
if (portletRegistry == null) {
println(" ! ERROR: Could not find PortletRegistry service. Aborting.")
return
}
println("--- STARTING CLEANUP (Flat JSON + Auto Publish) (Dry Run: ${dryRun}) ---")
List<FragmentEntryLink> allLinks = FragmentEntryLinkLocalServiceUtil.getFragmentEntryLinks(-1, -1)
Set<Long> infectedLinkIds = new HashSet<>()
Map<Long, Set<Long>> contentPagePlidMap = new HashMap<>()
println(" > Step 1: Scanning ${allLinks.size()} FragmentEntryLinks...")
for (FragmentEntryLink link : allLinks) {
if (targetGroupId > 0 && link.getGroupId() != targetGroupId) continue;
try {
boolean match = false
List<String> pIds = portletRegistry.getFragmentEntryLinkPortletIds(link)
if (pIds != null) {
for (String pid : pIds) {
if (pid.startsWith(targetPortletId)) { match = true; break; }
}
}
if (!match && link.getRendererKey() != null && link.getRendererKey().startsWith(targetPortletId)) {
match = true
}
if (match) {
infectedLinkIds.add(link.getFragmentEntryLinkId())
long plid = link.getPlid()
if (plid > 0) {
if (!contentPagePlidMap.containsKey(plid)) {
contentPagePlidMap.put(plid, new HashSet<Long>())
}
contentPagePlidMap.get(plid).add(link.getFragmentEntryLinkId())
}
}
} catch (Exception e) {}
}
println(" > Found ${infectedLinkIds.size()} infected FragmentEntryLinks.")
println(" > Impacted Content Pages (PLIDs): ${contentPagePlidMap.size()}")
int contentPagesFixed = 0
if (!contentPagePlidMap.isEmpty()) {
println("\n > Step 2: Fixing Content Pages...")
for (Long plid : contentPagePlidMap.keySet()) {
try {
Set<Long> pageBadLinks = contentPagePlidMap.get(plid)
Layout layout = LayoutLocalServiceUtil.fetchLayout(plid)
if (layout == null) continue
LayoutPageTemplateStructure structure = LayoutPageTemplateStructureLocalServiceUtil.fetchLayoutPageTemplateStructure(layout.getGroupId(), plid)
if (structure != null) {
List<LayoutPageTemplateStructureRel> rels = LayoutPageTemplateStructureRelLocalServiceUtil.getLayoutPageTemplateStructureRels(structure.getLayoutPageTemplateStructureId())
for (LayoutPageTemplateStructureRel rel : rels) {
String data = structure.getData()
if (data == null || data.isEmpty()) continue
JSONObject root = JSONFactoryUtil.createJSONObject(data)
boolean modified = cleanNormalizedStructure(root, pageBadLinks)
if (modified) {
println(" > [CONTENT PAGE] Fixing: ${layout.getName(\"en_US\")} (PLID: ${plid})")
if (!dryRun) {
LayoutPageTemplateStructureLocalServiceUtil.updateLayoutPageTemplateStructureData(
layout.getGroupId(),
plid,
rel.getSegmentsExperienceId(),
root.toString()
)
layout = LayoutLocalServiceUtil.fetchLayout(plid)
if (layout.getStatus() != WorkflowConstants.STATUS_APPROVED) {
layout.setStatus(WorkflowConstants.STATUS_APPROVED)
LayoutLocalServiceUtil.updateLayout(layout)
}
contentPagesFixed++
}
}
}
}
} catch (Exception e) {
println(" ! Error processing PLID ${plid}: ${e.getMessage()}")
}
}
}
int widgetPagesFixed = 0
println("\n > Step 3: Scanning for Widget Pages...")
List<Layout> allLayouts
if (targetGroupId > 0) {
allLayouts = LayoutLocalServiceUtil.getLayouts(targetGroupId, false)
allLayouts.addAll(LayoutLocalServiceUtil.getLayouts(targetGroupId, true))
} else {
allLayouts = LayoutLocalServiceUtil.getLayouts(-1, -1)
}
for (Layout layout : allLayouts) {
if (layout.isTypePortlet()) {
try {
LayoutTypePortlet ltp = (LayoutTypePortlet) layout.getLayoutType()
List<String> portletIds = new ArrayList<>(ltp.getPortletIds())
boolean layoutModified = false
for (String pid : portletIds) {
if (pid.startsWith(targetPortletId)) {
println(" > [WIDGET PAGE] Fixing: ${layout.getName(\"en_US\")} (ID: ${layout.getLayoutId()})")
if (!dryRun) {
ltp.removePortletId(layout.getUserId(),pid)
layoutModified = true
}
}
}
if (layoutModified && !dryRun) {
LayoutLocalServiceUtil.updateLayout(layout.getGroupId(), layout.isPrivateLayout(), layout.getLayoutId(), layout.getTypeSettings())
widgetPagesFixed++
}
} catch (Exception e) {}
}
}
int linksDeleted = 0
if (!dryRun && !infectedLinkIds.isEmpty()) {
println("\n > Step 4: Deleting orphaned FragmentEntryLinks...")
for (Long linkId : infectedLinkIds) {
try {
FragmentEntryLinkLocalServiceUtil.deleteFragmentEntryLink(linkId)
linksDeleted++
} catch (Exception e) {
println(" ! Error deleting link ${linkId}: ${e.getMessage()}")
}
}
}
serviceTracker.close()
println("\n--- SUMMARY ---")
println("Content Pages Fixed: ${contentPagesFixed}")
println("Widget Pages Fixed: ${widgetPagesFixed}")
println("Links Deleted: ${linksDeleted}")
boolean cleanNormalizedStructure(JSONObject root, Set<Long> badLinkIds) {
if (!root.has("items")) return false;
JSONObject itemsMap = root.getJSONObject("items");
Iterator<String> keys = itemsMap.keys();
List<String> itemsToRemove = new ArrayList<>();
while (keys.hasNext()) {
String itemId = keys.next();
JSONObject item = itemsMap.getJSONObject(itemId);
if (item.has("config")) {
JSONObject config = item.getJSONObject("config");
if (config.has("fragmentEntryLinkId")) {
String linkIdStr = config.getString("fragmentEntryLinkId");
if (linkIdStr != null && !linkIdStr.isEmpty()) {
try {
long linkId = Long.parseLong(linkIdStr);
if (badLinkIds.contains(linkId)) {
itemsToRemove.add(itemId);
}
} catch (NumberFormatException nfe) {
}
}
}
}
}
if (itemsToRemove.isEmpty()) return false;
for (String itemIdToRemove : itemsToRemove) {
JSONObject itemToRemove = itemsMap.getJSONObject(itemIdToRemove);
String parentId = itemToRemove.getString("parentId");
if (parentId != null && !parentId.isEmpty() && itemsMap.has(parentId)) {
JSONObject parentObj = itemsMap.getJSONObject(parentId);
if (parentObj.has("children")) {
JSONArray children = parentObj.getJSONArray("children");
JSONArray newChildren = JSONFactoryUtil.createJSONArray();
for (int i = 0; i < children.length(); i++) {
String childId = children.getString(i);
if (!childId.equals(itemIdToRemove)) {
newChildren.put(childId);
}
}
parentObj.put("children", newChildren);
}
}
itemsMap.remove(itemIdToRemove);
}
return true;
}