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

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

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

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

Начиная с релиза 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 и написать более адекватный алгоритм, но лень.

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

  1. Pingback: Здравствуй, зимнее время! Часть II | Documentum in a (nuts)HELL
  2. Проблема контрола WDK как-то связана с версиями Java на раболчих станциях? Интересно, что сдвиг на два часа проявляется только у некоторых пользователей.

    Like

  3. Проблема контрола WDK как-то связана с версиями Java на раболчих станциях?

    Нет

    Интересно, что сдвиг на два часа проявляется только у некоторых пользователей.

    В самом документуме то как работает “время” определяется версиями (вплоть до патчей) операционной системы, базы, контент-сервера, jvm, если подключить еще wdk, то ко всему этому прибавляется еще версия операционной системы клиента и браузера – т.е. разброс конфигураций огромен, кроме этого есть пользователи которые любят жаловаться, а есть которые не любят – что добавляет проблем в сбор статистики.

    Like

  4. Pingback: Число прописью в java | Documentum in a (nuts)HELL

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s