понедельник, 31 декабря 2012 г.

HTML5 offline web application with GWT Linker

   Как научить веб-проложения работать как в online, так и в offline режимах? С развитием HTML 5 технологии у разработчиков появилась возможность создавать свои приложения работающие в автономном режиме. Такая особенность кажется очень привлекательной. Что же нужно сделать, чтобы добавить такую функциональность в GWT приложения? Рассмотрим возможность добавления offline режима. 
     Для начала рассмотрим, как приложения работают в offline режиме. Иногда, по разным причинам, когда пользователи работают с веб-приложением пропадает интернет и все взаимодействия приостанавливаются. Для решения этой проблемы разработчику необходимо определить manifest файл(декларация кэша), где перечислены все необходимые ресурсы для автономного режима. Например, представим себе простой апплет погоды состоящий из HTML страницы “weather.html”, файла CSS стилей “weather.css” и JavaScript файла “weather.js”. До того как мы добавим эти файлы в manifest, они будут находится в каталоге все по отдельности. Если пользователь попытается открыть файл weather.html в offline режиме, то получит ошибку. Но можно дописать в manifest эти три файла, как “clock.appcache”. После этого, когда пользователь попытается еще раз открыть страницу с апплетом, то браузер закеширует ресурсы и сделает их доступными, даже если пользователь обратиться к странице в автономном режиме. Полная документация по offline web application
     Чтобы GWT приложение могло работать подобным образом, необходимо кроме файла декларации кэша (в нем описываются файлы и запросы приложения, которые будут закешированны) написать свой Linker(компоновщик). 
     GWT компилирует и оптимизирует код для каждого браузера и сохраняет скомпилированный код в соответствующем каталоге. Все файлы создаются на основе контрольной суммы контента. Проблема заключается в том, что каждый раз, когда были сделаны изменения или перекомпиляция, необходимо также изменить имя файла манифейста декларации кэша. Одно из решений состоит в том, чтобы написать свой Linker. Что же такое linker и как его написать?
     Рассмотрим последовательные шаги по созданию linker-а:
- определить каталог, где будет размещен компоновщик
- создать класс наследуемый от AbstractLinker или Linker
- установить порядок использования LinkerOrder.
Аннотация позволяет устанавливать приоритет выполнения.
Linker.Order.Post означает выполнение после первичного компоновщика.
Linker.Order.Pre означает выполнения до первичного компоновщика.
Linker.Order.Primary означает, что компоновщик будет выполнен вместо первичного.
- создать новый файл модуля <module>.gwt.xml.
     После создания linker-а для его использования необходимо выполнить следующее: 
- наследовать его внутри основного модуля <main_module>.gwt.xml 
- и добавить linker <add-linker name=”my_linker_name” />
     Компоновщик PersonalLinker создан по примеру SimpleAppCacheLinker из GWT trunk code.
/**
* Linker for public path resources in the Application cache.
* based on code from com.google.gwt.core.linker.SimpleAppCacheLinker
*
* @author Dmitry Nikolaenko
*
*/
@LinkerOrder(LinkerOrder.Order.POST)
public class PersonalLinker extends AbstractLinker {

    private static final String MANIFEST = "spaceshootercache.manifest";
    private static final String DEFAULT_MANIFEST_TEMPLATE = "cache.manifest.template";
    private static final String CONFIGURATION_PROPERTY = "cache.manifest";

    @Override
    public ArtifactSet link(TreeLogger logger, LinkerContext context,
            ArtifactSet artifacts, boolean onePermutation) throws UnableToCompleteException {

        // provides stable ordering and de-duplication of artifacts
        // that we can modify and return
        ArtifactSet artifact = new ArtifactSet(artifacts);

        if (onePermutation) {
            return artifact;
        }

        // Find all Artifacts assignable to some base type.
        // The returned value will be a snapshot of the values
        // in the ArtifactSet.
        if (artifact.find(SelectionInformation.class).isEmpty()) {
            logger.log(TreeLogger.INFO, "Warning: " + MANIFEST +
                    " to allow debuging. " + "Recompile before deploying your app!");
            artifacts = null;
        } else {
            // create the general cache-manifest resource for the landing page:
            artifact.add(emitLandingPageCacheManifest(context, logger, artifacts));
        }

        return artifact;
    }

    @Override
    public String getDescription() {
        return "My personal linker description";
    }

    /**
     * Creates the cache-manifest resource specific for the landing page.
     *
     * @param context the linker environment
     * @param logger the tree logger to record to
     * @param artifacts {@code null} to generate an empty cache manifest
     */
    private Artifact<?> emitLandingPageCacheManifest(LinkerContext context,
            TreeLogger logger, ArtifactSet artifacts)
             throws UnableToCompleteException {
        StringBuilder publicSourcesSb = new StringBuilder();
        StringBuilder staticResoucesSb = new StringBuilder();

        if (artifacts != null) {
         // Iterate over all emitted artifacts, and collect all cacheable artifacts
         for (@SuppressWarnings("rawtypes") Artifact artifact : artifacts) {
            if (artifact instanceof EmittedArtifact) {
             EmittedArtifact ea = (EmittedArtifact) artifact;
             String pathName = ea.getPartialPath();
             if (pathName.endsWith("symbolMap")
                 || pathName.endsWith(".xml.gz")
                 || pathName.endsWith("rpc.log")
                 || pathName.endsWith("gwt.rpc")
                 || pathName.endsWith("manifest.txt")
                 || pathName.startsWith("rpcPolicyManifest")) {
                // skip these resources
             } else {
                publicSourcesSb.append(pathName + "\n");
             }
            }
         }

         String[] cacheExtraFiles = getPropertiesExtraFiles(context);

         if (cacheExtraFiles.length == 0) {
             cacheExtraFiles = getCacheExtraFiles();
         }

         for (int i = 0; i < cacheExtraFiles.length; i++) {
            staticResoucesSb.append(cacheExtraFiles[i]);
            staticResoucesSb.append("\n");
         }
        }

        String cacheManifest = createApplicationCache(logger, context, publicSourcesSb, staticResoucesSb);

        logger.log(TreeLogger.INFO, "Be sure your landing page's <html> tag declares a manifest:"
            + " <html manifest=" + context.getModuleFunctionName() + "/" + MANIFEST + "\">");

        // Create the manifest as a new artifact and return it:
        return emitString(logger, cacheManifest, MANIFEST);
    }

    /**
     * Create application cache manifest file
     *
     * @param logger the tree logger to record to
     * @param context the linker environment
     * @param publicSourcesSb public resources that generated by the compilation
     * @param staticResoucesSb static resources
     * @return
     * @throws UnableToCompleteException
     */
    private String createApplicationCache(TreeLogger logger, LinkerContext context,
            StringBuilder publicSourcesSb, StringBuilder staticResoucesSb)
                throws UnableToCompleteException {
        try {
            String manifest = IOUtils.toString(getClass().getResourceAsStream(getCacheManifestTemplate()));
  
            // replace placeholder with the real data
            StringBuilder sbData = new StringBuilder();
            sbData.append(new Date().getTime());
            sbData.append(".");
            sbData.append(Math.random());
  
            manifest = manifest.replace("$UNIQUEAPPID$", sbData.toString());
            manifest = manifest.replace("$STATICAPPFILES$", staticResoucesSb.toString());
            manifest = manifest.replace("$GENAPPFILES$", publicSourcesSb.toString());
  
            return manifest;
        } catch(IOException ex) {
            logger.log(TreeLogger.ERROR, "Cound not read cache manifest file", ex);
            throw new UnableToCompleteException();
        }
    }

    /**
     * You should include this file into <html manifest="" \>
     */
    protected String getCacheManifestTemplate() {
        return DEFAULT_MANIFEST_TEMPLATE;
    }

    /**
     * Obtains the extra files to include in the manifest.
     * Ensures the returned array is not null.
     */
    protected String[] getCacheExtraFiles() {
        String[] cacheExtraFiles = otherCachedFiles();
        return cacheExtraFiles == null ? new String[0] :
            Arrays.copyOf(cacheExtraFiles, cacheExtraFiles.length);
    }

    /**
     * Override this method to force the linker to also include more files in
     * the manifest.
     */
    protected String[] otherCachedFiles() {
        return null;
    }

    /**
     * Get array of configured external properties
     *
     * @param context the linker environment
     * @return external properties
     */
    protected String[] getPropertiesExtraFiles(LinkerContext context) {
        Set<ConfigurationProperty> properties = context.getConfigurationProperties();

        // if properties is empty - no external options, cache.manifest is empty!
        if (!properties.isEmpty()) {
            for (ConfigurationProperty property : properties) {
                if (property.getName().equalsIgnoreCase(CONFIGURATION_PROPERTY)) {
                    List<String> props = property.getValues();
                    return props.toArray(new String[0]);
                }
            }
        }

        return new String[0];
    }
}


* This source code was highlighted with Source Code Highlighter.
     Далее необходимо создать файл модуль компоновщика, где он будет зарегистрирован.
<module>
  <define-linker name="personalManifestLinker"
     class="com.dmitrynikol.spaceshooter.client.linker.PersonalLinker" />
</module>


* This source code was highlighted with Source Code Highlighter.
     Напишем компоновщик приложения наследуя главный PersonalLinker.
@Shardable
public class ApplicationCacheManifestLinker extends PersonalLinker {

    @Override
    protected String[] getCacheExtraFiles() {
        return new String[] { "SpaceShooter.html", "SpaceShooter.css" };
    }

    @Override
    protected String getCacheManifestTemplate() {
        return "spaceshooter.appcache.manifest";
    }
}


* This source code was highlighted with Source Code Highlighter.
     В переопределенных методах getCacheExtraFiles() и getCacheManifestTemplate() указываются кешируемые файлы и название файла шаблона манифеста. Если шаблон не был определен, то будет использоваться шаблон определенный по умолчанию в PersonalLinker. Написанный компоновщик следует добавить в главный модуль приложения <main_app_module>.gwt.xml:
<define-linker name="personalManifestLinker"
         class="com.dmitrynikol.spaceshooter.client.linker.ApplicationCacheManifestLinker" />
 <add-linker name="personalManifestLinker" />


* This source code was highlighted with Source Code Highlighter.
     Manifest файл приложения необходимо дописать в главный html файл, тем самым мы сообщим браузеру откуда считывать файл.
<html manifest="spaceshooter/spaceshootercache.manifest">

* This source code was highlighted with Source Code Highlighter.
     Также в web.xml необходимо добавить следующие строки:
<mime-mapping>
    <extension>personalManifestLinker</extension>
    <mime-type>text/cache-manifest</mime-type>
</mime-mapping>


* This source code was highlighted with Source Code Highlighter.
     Теперь linker приложения готов к использованию. После компилирования в war-нике можно будет увидеть сгенерированный spaceshootercache.manifest вместе с остальными файлами. При запуске приложения в браузере, который поддерживает Offline Web Applications стандарт, можно увидеть панель предлагающую хранить данные на компьютере пользователя для использования в автономном режиме.
     Readme и исходный код доступен на github.