Thread Dump JSP

I was trying to google first and found some implementations:

unfortunately, no one displays lock information properly, so I wrote my own.

<%@ page import="java.io.IOException" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.lang.management.LockInfo" %>
<%@ page import="java.lang.management.ManagementFactory" %>
<%@ page import="java.lang.management.MonitorInfo" %>
<%@ page import="java.lang.management.ThreadInfo" %>
<%@ page import="java.lang.management.ThreadMXBean" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.Collections" %>
<%@ page import="java.util.List" %>

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
    public class MyThreadInfo implements Comparable<MyThreadInfo> {

        private final long threadID;

        private final ThreadInfo threadInfo;

        private final String name;

        public MyThreadInfo(ThreadInfo info) {
            threadInfo = info;
            name = info.getThreadName();
            threadID = info.getThreadId();
        }

        public int compareTo(MyThreadInfo o) {
            String myName = name + threadID;
            String yourName = o.name + o.threadID;
            return myName.compareTo(yourName);
        }

    }

    public List<MyThreadInfo> getThreads(boolean deadlock) {
        ThreadMXBean tmb = ManagementFactory.getThreadMXBean();
        ArrayList<MyThreadInfo> threads = new ArrayList<MyThreadInfo>();
        long[] threadIds = null;
        if (deadlock) {
            threadIds = tmb.findDeadlockedThreads();
        } else {
            threadIds = tmb.getAllThreadIds();
        }
        if (threadIds == null) {
            return Collections.emptyList();
        }
        for (ThreadInfo info : tmb.getThreadInfo(threadIds, true, true)) {
            if (info != null) {
                threads.add(new MyThreadInfo(info));
            }

        }
        Collections.sort(threads);
        return threads;
    }

    public String toString(ThreadInfo threadInfo) {
        StringBuilder sb = new StringBuilder("\"" + threadInfo.getThreadName() + "\"" +
                " Id=" + threadInfo.getThreadId() + " " +
                threadInfo.getThreadState());
        if (threadInfo.getLockName() != null) {
            sb.append(" on ").append(threadInfo.getLockName());
        }
        if (threadInfo.getLockOwnerName() != null) {
            sb.append(" owned by \"").append(threadInfo.getLockOwnerName())
                    .append("\" Id=").append(threadInfo.getLockOwnerId());
        }
        if (threadInfo.isSuspended()) {
            sb.append(" (suspended)");
        }
        if (threadInfo.isInNative()) {
            sb.append(" (in native)");
        }
        sb.append('\n');
        StackTraceElement[] stackTrace = threadInfo.getStackTrace();
        for (int i = 0, n = stackTrace.length; i < n; i++) {
            StackTraceElement ste = stackTrace[i];
            sb.append("\tat ").append(ste.toString());
            sb.append('\n');
            if (i == 0 && threadInfo.getLockInfo() != null) {
                Thread.State ts = threadInfo.getThreadState();
                switch (ts) {
                    case BLOCKED:
                        sb.append("\t-  blocked on ").append(threadInfo.getLockInfo());
                        sb.append('\n');
                        break;
                    case WAITING:
                        sb.append("\t-  waiting on ").append(threadInfo.getLockInfo());
                        sb.append('\n');
                        break;
                    case TIMED_WAITING:
                        sb.append("\t-  waiting on ").append(threadInfo.getLockInfo());
                        sb.append('\n');
                        break;
                    default:
                }
            }

            for (MonitorInfo mi : threadInfo.getLockedMonitors()) {
                if (mi.getLockedStackDepth() == i) {
                    sb.append("\t-  locked ").append(mi);
                    sb.append('\n');
                }
            }
        }

        LockInfo[] locks = threadInfo.getLockedSynchronizers();
        if (locks.length > 0) {
            sb.append("\n\tNumber of locked synchronizers = ").append(locks.length);
            sb.append('\n');
            for (LockInfo li : locks) {
                sb.append("\t- ").append(li);
                sb.append('\n');
            }
        }
        sb.append('\n');
        return sb.toString();
    }

    public void printThreads(List<MyThreadInfo> threads, JspWriter out) throws IOException {
        out.println("<br><b>Total threads: " + threads.size() + "</b>");
        for (MyThreadInfo mti : threads) {
            out.println("<pre>");
            out.print(toString(mti.threadInfo));
            out.println("</pre>");
        }
    }

%>
<html>
<body>
<table>
    <tr>
        <td><b>Date:</b></td>
        <td><%=new java.util.Date()%>
        </td>
    </tr>
</table>

<%
    try {
        printThreads(getThreads(false), out);
        List<MyThreadInfo> deadLockedThreads = getThreads(true);
        if (deadLockedThreads != null && !deadLockedThreads.isEmpty()) {
            out.print("<br><b>Found deadlocks:<b>");
            printThreads(deadLockedThreads, out);
        }
    } catch (Exception e) {
        out.print("<pre>");
        e.printStackTrace(new PrintWriter(out, true));
        out.print("</pre>");
    }
%>
</body>
</html>

Здравствуй, зимнее время!

Этот пост навряд ли будет интересен англоязычным пользователям, в особенности тем, чья страна покрывается полностью одним часовым поясом, поэтому пишу на великом и могучем.

Итак, ваше (ага, я уже два года живу в Мельбурне, и у нас сейчас весна, поэтому ваше) правительство в очередной раз подложило свинью всем айтишникам и таки вернуло летнее время взад. Что интересно, так это то, что вендоры подготовили патчи для своих продуктов относительно давно, но коллеги в скайпе активизировались только на этой неделе.

Кратко о том, как управляет временем документум.

Начиная с релиза 6.0 разработчики решили, что хранить в СУБД локальные даты не круто, и начали в базу писать время в UTC, поведение контролируется атрибутом r_normal_tz в dm_server_config: 0 – пишем в UTC, не 0 – пишем локальное время. Кроме этого в dm_server_config есть параметр r_tz_aware который определяет каким образом CS общается с DFC: T – между клиентом и сервером время ходит в формате iso8601 (время передается в UTC), F – в формате “xxx MMM d HH:mm:ss yyyy” (передается локальное время). Вроде бы все логично (хотя при таком подходе страдают отчетные системы, работающие с СУБД напрямую), однако, и тут разработчики умудрились накосячить (здесь пассаж относительно “указанный уровень работы Content Server-а был спроектирован давно” мне оказался так и не ясен, поскольку 6.0 вышел сравнительно недавно):

т.е. кто сидит на версиях ниже 6.7SP1P22 и 6.7SP2P08 постоянно огребает проблемы со временем.

О проблемах в WDK

Собственно, коллеги нарвались на совершенно безумную реазилацию поддержки часовых поясов в WDK. Для начала, примеры систем, которые делаются программистами для людей:

Youtrack:

Jira:

Ну, т.е. все просто – пользователь выставляет в настройках часовой пояс и наступает счастье. Теперь настройки Webtop:

Не густо :(. Как реализованы часовые пояса в Webtop?

в /wdk/redirect.jsp следующий код передает разницу в минутах между часовым поясом клиента и UTC:

    <script type="text/javascript">
        function onLoad() {
            <%--
            // an image.--%>
            var strUrl = new Object;
            var strUrlString = addBrowserIdToURL('<%=strUrl%>');
            strUrl.src = addBrowserIdToURL(strUrlString)
                    + "&<%=IParams.TIME_ZONE_OFFSET%>=" + new Date().getTimezoneOffset();
            navigateToURL(strUrl.src, "redirectForm", window)
        }
    </script>

Далее этот параметр (__dmfTzoff) обрабатывается в com.documentum.web.env.EnvironmentService#notifyRequestStart и вызывается com.documentum.web.common.LocaleService#setTimeZone, где по смещению в минутах разработчики пытаются определить часовой пояс клиента используя следующий забавный алгоритм:

перебираем все часовые пояса из java.util.TimeZone#getAvailableIDs() и выбираем первый, смещение которого совпадает с переданным

И тут нас ждет сюрприз (пример для Калининграда):

import java.util.Calendar;
import java.util.TimeZone;

/**
 * @author Andrey B. Panfilov <andrew@panfilov.tel>
 */
public class Test {

	public static void main(String[] args) throws Exception {
		long offset = 2 * 60 * 1000 * 60;
		Calendar calendar = Calendar.getInstance();
		calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) + 1);
		for (String tzid : TimeZone.getAvailableIDs()) {
			TimeZone tz = TimeZone.getTimeZone(tzid);
			if (getOffset(calendar.getTimeInMillis(), tz) == offset) {
				System.out.println("Offset: " + offset + ", TZ: " + tzid);
			}
		}
	}

	private static long getOffset(long timestamp, TimeZone tz) {
		return tz.getOffset(timestamp);
	}

}

результат:

Offset: 7200000, TZ: Africa/Windhoek
Offset: 7200000, TZ: ART
Offset: 7200000, TZ: Africa/Blantyre
Offset: 7200000, TZ: Africa/Bujumbura
...
Offset: 7200000, TZ: Europe/Tiraspol
Offset: 7200000, TZ: Europe/Uzhgorod
Offset: 7200000, TZ: Europe/Vilnius
Offset: 7200000, TZ: Europe/Zaporozhye
Offset: 7200000, TZ: Israel
Offset: 7200000, TZ: Libya
Offset: 7200000, TZ: Turkey
Offset: 7200000, TZ: Europe/Kaliningrad

Первым в списке идет Africa/Windhoek, соответственно, для пользователей Калининграда, работающих с Webtop, раположенном в Москве, Webtop будет считать, что часовой пояс у пользователя – Africa/Windhoek, а там сейчас не зима, а лето, и у пользователя летние даты будут отображаться со смещением на два часа вперед.

Собственно, я, не особо долго думая сделал такой патч для com.documentum.web.common.LocaleService:

    private static List<String> s_preferredTimeZones = null;

    private static final String PREFERRED_TIMEZONES_CONFIG_ELEMENT 
                                  = "application.preferred_timezones";

    private static final String TIMEZONE_CONFIG_ELEMENT = "timezone";

    public static synchronized List<String> getPreferredTimezones() {
        if (s_preferredTimeZones == null) {
            s_preferredTimeZones = new ArrayList<String>();
            IConfigLookup lookup = ConfigService.getConfigLookup();
            IConfigElement configTimeZones = lookup.lookupElement(
                    PREFERRED_TIMEZONES_CONFIG_ELEMENT, getContext());
            if (configTimeZones == null) {
                return Collections.unmodifiableList(s_preferredTimeZones);
            }
            for (Iterator iter = configTimeZones
                    .getChildElements(TIMEZONE_CONFIG_ELEMENT); iter.hasNext();) {
                IConfigElement timeZone = (IConfigElement) iter.next();
                String name = timeZone.getValue();
                if (name != null) {
                    s_preferredTimeZones.add(name);
                }
            }

        }
        return Collections.unmodifiableList(s_preferredTimeZones);
    }

    public static void setTimeZone(int clientTzOffset) {
        if (!hasClientTimeZoneEnabled()) {
            return;
        }
        if (SessionState.getAttribute(TIMEZONE_SESSION_VAR) != null) {
            return;
        }

        clientTzOffset = -clientTzOffset * 60 * 1000;

        long now = System.currentTimeMillis();
        TimeZone clientTz = TimeZone.getDefault();
        if (clientTz.getOffset(now) != clientTzOffset) {
            List<String> preferredTimeZones = getPreferredTimezones();
            List<TimeZone> zones = new ArrayList<TimeZone>();
            boolean found = false;
            for (String tzid : TimeZone.getAvailableIDs()) {
                TimeZone tz = TimeZone.getTimeZone(tzid);
                if (tz.getOffset(now) != clientTzOffset) {
                    continue;
                }
                if (preferredTimeZones.contains(tzid)) {
                    clientTz = tz;
                    found = true;
                    break;
                }
                zones.add(tz);
            }
            if (!found && !zones.isEmpty()) {
                clientTz = zones.get(0);
            }
        }
        SessionState.setAttribute(TIMEZONE_SESSION_VAR, clientTz);
        //Забавно, но две строчки ниже никогда не работают, потому что первый вызов
        //идет со страницы логина, поэтому запись в RepositoryPreferencesStore не 
        //осуществляется - логин пользователя не известен, а потом до этого места
        //не доходит из-за проверок SessionState
        IPreferenceStore prefs = PreferenceService.getPreferenceStore();
        prefs.writeString(TIMEZONE_PREFERENCE, clientTz.getID());
        if (Trace.LOCALESERVICE) {
            Trace.println("LocaleService: Time zone set to "
                    + clientTz.getDisplayName());
        }
    }

и пишу в app.xml следующее:

            <preferred_timezones>
                <timezone>Europe/Kaliningrad</timezone>
                <timezone>Europe/Moscow</timezone>
            </preferred_timezones>

Вообще не плохо было бы подключить ICU и написать более адекватный алгоритм, но лень.

Dynamic groups. Advances. Part IV

Last week I was trying to solve the problem related to ugly security model in Documentum: business user must able to see all documents related to workflow task which is assigned to him. Actually, it’s a true “case management” problem: someone sent me a task, that task has related documents, those documents have another relations and so on – to be able to perform the task I should able to see all related documents. Unfortunately, Documentum does not have any OOTB instruments/functions to resole such problem, for example, take a look at Webtop – it has some predefined distribution workflows (dmSendTo*), but to take advantage of these workflows supervisor must explicitly grant access permissions to performers on related documents, yes, when designing workflows with BPM we can add special automatic activities which grant required permissions to manual performers, but what to do with delegations (both manual and automatic) and chains of related documents?

My WDK solutions was:

PrivilegedFormProcessor:

public class PrivilegedFormProcessor extends FormProcessor {

    public PrivilegedFormProcessor() {
        super();
    }

    @Override
    public void processAction(PageContext pageContext, String formClass,
            String nlsClass) {
        boolean notifyAtEnd = notifyStart(pageContext);
        try {
            super.processAction(pageContext, formClass, formClass);
        } finally {
            if (notifyAtEnd) {
                notifyFinish(pageContext);
            }
        }
    }

}

PrivilegedRequestListener:

@SuppressWarnings("deprecation")
public class PrivilegedRequestListener implements IFormRenderListener,
        com.documentum.web.form.IRequestListener, IApplicationListener,
        com.documentum.web.env.IRequestListener {

    private static final int RECURSION_MAX_DEPTH = 2;

    private static final ThreadLocal<DfRoleSpec> PRIVILEGED_GROUP = new ThreadLocal<DfRoleSpec>();

    public PrivilegedRequestListener() {
        super();
    }

    @Override
    public void notifyRequestStart(HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse) {
        startPrivilegedRequest(httpServletRequest, null);
    }

    @Override
    public void notifyRequestFinish(HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse) {
        stopPrivilegedRequest(httpServletRequest);
    }

    @Override
    public void notifyFormRenderStart(Form form) {
        startPrivilegedRequest(form.getPageContext().getRequest(),
                form.getPageContext());
    }

    @Override
    public void notifyFormRenderFinish(Form form) {
        stopPrivilegedRequest(form.getPageContext().getRequest());
    }

    @Override
    @SuppressWarnings("deprecation")
    public void notifyStart(PageContext context) {
        startPrivilegedRequest(context.getRequest(), context);
    }

    @Override
    @SuppressWarnings("deprecation")
    public void notifyFinish(PageContext context) {
        stopPrivilegedRequest(context.getRequest());
    }

    @Override
    @SuppressWarnings("deprecation")
    public void notifyApplicationStart(ServletContext servletContext) {
        PrivilegedFormProcessor.addRequestListener(this);
    }

    @Override
    public void notifyApplicationFinish(ServletContext servletContext) {

    }

    public static void startPrivilegedRequest(ServletRequest request,
            PageContext context) {
        if (isPrivilegedRequest()) {
            return;
        }
        Form form = getForm(request, context);
        if (form == null) {
            return;
        }
        IPrivilegedComponent component = ControlUtils.findEnclosingControl(
                IPrivilegedComponent.class, form);
        if (component == null) {
            Control handler = getHandler(context, form);
            if (handler != null) {
                component = ControlUtils.findEnclosingControl(
                        IPrivilegedComponent.class, handler);
            }
        }
        if (component == null) {
            component = getPrivilegedCallerForm(form, RECURSION_MAX_DEPTH);
        }
        if (component == null) {
            return;
        }
        String privilegedGroup = component.getPrivilegedGroupName();
        if (StringUtils.isBlank(privilegedGroup)) {
            return;
        }
        String docbaseName = SessionManagerHttpBinding.getCurrentDocbase();
        if (StringUtils.isBlank(docbaseName)) {
            return;
        }
        RoleRequestManager requestManager = RoleRequestManager.getInstance();
        DfRoleSpec roleSpec = new DfRoleSpec(privilegedGroup, docbaseName);
        requestManager.push(roleSpec);
        PRIVILEGED_GROUP.set(roleSpec);
    }

    public static void stopPrivilegedRequest(ServletRequest request) {
        if (!isPrivilegedRequest()) {
            return;
        }

        RoleRequestManager requestManager = RoleRequestManager.getInstance();
        requestManager.pop(PRIVILEGED_GROUP.get());
        PRIVILEGED_GROUP.remove();
    }

    private static Form getForm(ServletRequest request, PageContext context) {
        Form form = (Form) request.getAttribute(IParams.FORM);
        if (form != null) {
            return form;
        }
        if (context == null) {
            return null;
        }
        FormRequest formRequest = (FormRequest) context.getAttribute(
                IParams.FORM_REQUEST, PageContext.REQUEST_SCOPE);
        if (formRequest != null) {
            return formRequest.getForm();
        }
        FormHistory history = (FormHistory) context.getAttribute(
                IParams.FORM_HISTORY, PageContext.REQUEST_SCOPE);
        if (history != null) {
            return history.getCurrentSnapshot().getForm();
        }

        Map historyMap = getFormHistoryMap(context);
        if (historyMap == null) {
            return null;
        }
        String requestId = request.getParameter(IParams.REQUEST_ID);
        if (StringUtils.isBlank(requestId)) {
            return null;
        }
        history = (FormHistory) historyMap.get(new FormRequestId(requestId)
                .getClientId());
        if (history == null) {
            return null;
        }
        return history.getCurrentSnapshot().getForm();
    }

    private static Map getFormHistoryMap(PageContext pageContext) {
        synchronized (pageContext.getSession()) {
            FormProcessor.HistoryByClientIdMap mapHistory = (FormProcessor.HistoryByClientIdMap) pageContext
                    .getAttribute("__dmfFormHistoryByClientId",
                            PageContext.SESSION_SCOPE);
            if (mapHistory != null) {
                return mapHistory.getHistoryByClientIdMap();
            }
            return null;
        }
    }

    private static Control getHandler(PageContext context, Form form) {
        String formId = Util.getRequestParameter(context.getRequest(),
                IParams.FORM_ID);
        String handlerParam = Util.getRequestParameter(context.getRequest(),
                IParams.HANDLER);
        String handlerName = null;
        if ((formId != null) && (handlerParam != null)
                && (handlerParam.startsWith(formId))) {
            handlerName = handlerParam.substring(formId.length() + 1);
        } else if (formId == null) {
            handlerName = handlerParam;
        }
        Control handler = form;
        if (handlerName == null) {
            return null;
        }
        if (!handler.getElementName().equals(handlerName)) {
            handler = form.getControlByElement(handlerName);
        }
        return handler;
    }

    private static IPrivilegedComponent getPrivilegedCallerForm(Form form,
            int depth) {
        Form callerForm = form.getCallerForm();
        if (callerForm == null) {
            return null;
        }
        if (callerForm instanceof IPrivilegedComponent) {
            return (IPrivilegedComponent) callerForm;
        }
        if (callerForm instanceof Container) {
            Container container = (Container) callerForm;
            String includedComponentName = container
                    .getContainedComponentName();
            if (StringUtils.isNotBlank(includedComponentName)) {
                Component component = ControlUtils.findInnerControl(
                        Component.class, container, includedComponentName);
                if (component instanceof IPrivilegedComponent) {
                    return (IPrivilegedComponent) component;
                }
            }
        }
        if (depth == 0) {
            return null;
        }
        return getPrivilegedCallerForm(callerForm, --depth);
    }

    public static boolean isPrivilegedRequest() {
        return PRIVILEGED_GROUP.get() != null;
    }

    public static String getPrivilegedGroup() {
        DfRoleSpec roleSpec = PRIVILEGED_GROUP.get();
        if (roleSpec == null) {
            return null;
        }
        return roleSpec.getRoleName();
    }

}

ControlUtils:

public class ControlUtils {

    private ControlUtils() {
        super();
    }

    public static <T> T findEnclosingControl(Class<T> cls, Control control) {
        FindEnclosingControl<T> visitor = new FindEnclosingControl<T>(cls);
        control.visitContainer(visitor);
        return visitor.getControl();
    }

    @SuppressWarnings("unchecked")
    static class FindEnclosingControl<E> implements IVisitor {
        private E _control;

        private Class<E> _controlClass;

        FindEnclosingControl(Class<E> cls) {
            _controlClass = cls;
        }

        public boolean visit(Control control) {
            boolean keepLooking = true;
            if (_controlClass.isInstance(control)) {
                _control = (E) control;
                keepLooking = false;
            }
            return keepLooking;
        }

        public E getControl() {
            return _control;
        }
    }

}

IPrivilegedComponent:

public interface IPrivilegedComponent {

    String getPrivilegedGroupName();

}

app.xml:

<listeners>
    <application-listeners>

        ...

        <listener>
            <class>com.documentum.web.form.PrivilegedRequestListener</class>
        </listener>
    </application-listeners>
    <request-listeners>

        ...

        <listener>
            <class>com.documentum.web.form.PrivilegedRequestListener</class>
        </listener>
    </request-listeners>
    <formrender-listeners>
        
        ...

        <listener>
            <class>com.documentum.web.form.PrivilegedRequestListener</class>
        </listener>
    </formrender-listeners>
</listeners>

WEB-INF/classes/com/documentum/web/form/FormProcessorProp.properties:

formProcessorClass=com.documentum.web.form.PrivilegedFormProcessor

and now, to solve my problem, I should just write something like:

public class TaskMgrContainerCustom extends TaskMgrContainer implements
        IPrivilegedComponent {

    public TaskMgrContainerCustom() {
        super();
    }

    @Override
    public String getPrivilegedGroupName() {
        return "dm_read_all_dynamic";
    }

}