Knowledge Base
Published Feb. 17, 2026

How to Completely Delete a Portlet and Its Associated Data

Written By

András Bakalár

How To articles are not official guidelines or officially supported documentation. They are community-contributed content and may not always reflect the latest updates to Liferay DXP. We welcome your feedback to improve How To articles!

While we make every effort to ensure this Knowledge Base is accurate, it may not always reflect the most recent updates or official guidelines.We appreciate your understanding and encourage you to reach out with any feedback or concerns.

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

Warning: The following solution involves running a script that directly modifies layout data and database records. Always perform a complete backup of your database before proceeding. It is highly recommended to run this in a lower environment and test thoroughly before executing in production.

Use a Groovy script to automate the removal process across your instance.

  1. Navigate to Control Panel → Server Administration → Script.

  2. Select Groovy as the language.

  3. Copy and paste the script below into the script editor.

  4. 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 to true for 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 as 0 to scan all sites.

  5. Click Execute and review the output in the "Result" section.

  6. Once you have verified the output of the dry run, change dryRun to false and 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;
}

Did this article resolve your issue ?

Knowledge Base