• Vladimír Kubinda
  • Software engineer
  • 3. Dec 2017

Wicket a dynamické resource id

Isto to všetci, čo pracujete na tvorbe klientského UI, poznáte. Upravíte niečo v CSS, načítate obrazovku a uvedená zmena sa neprejavila. Pozriete opäť do CSS súboru, skontrolujete a po chvíli si uvedomíte - “Sakra! Browser cache!”. V prehliadači dáte hard reload poprípade vymažete cache a všetko je ok. Počas vývoja to je len nepríjemná komplikácia. Horšie to však je, pokiaľ sa takáto vec vyskytne na ostrej aplikácii. Videl som už aj riešenia, kde na jednom nemenovanom webe vybehla notifikácia, že pre správne fungovanie stránky je potrebné vymazanie browser cache. No som zvedavý, ako si s tým poradil bežný použivateľ :).

Ideálne je mať takúto vec už od začiatku podchytenú tak, aby si zmeny resourcov aplikácie nevyžadovali ďalší zásah programátora. Keď sme prenedávnom programovali aplikáciu v jazyku Scala, na tvorbu klientského používateľského rozhrania sme použili framework Lift. Tam som si všimol jednu užitočnú vec a to špeciálny tag parameter, ktorý umožňoval explicitnú kontrolu nad resourcami poskytovanými aplikáciou, ako sú napr. CSS, JS, IMG atď.

Na nasledujúcich riadkoch vám ukážem, ako podobnú funkcionalitu naprogramovať s frameworkom Apache Wicket.

Do základnej stránky, ktorá definuje HTML šablónu vložím odkaz na kontrolovaný resource. Tým poviem, že chcem aktívne riadiť platnosť tohto css.

<link wicket:resource="with-resource-id" href="/css/main-theme.css" rel="stylesheet">

Ako je vidieť vyšsie, vytvoril som si vlastný wicket tag parameter “wicket:resource”, ktorému som dal hodnotu “with-resource-id”. Teraz potrebujem zaregistrovať tento parameter, aby ho wicket parser vedel rozpoznať a posunúť na ďalšie spracovanie. Ešte predtým si však definujme základnú logiku práce s riadenými resourcami. Pre jednoduchosť si dajme za cieľ, aby každý riadený resource bol nanovo načítaný u každého používateľa vždy, keď sa nasadí nová verzia aplikácie. Túto logiku zachytím nasledovne v triede ResourceId, ktorá má za cieľ riadiť platnosť resourcov, prostredníctvom generovaného resource id.

public class ResourceId {

	private static int hashCode = new Date().hashCode();

	public static String getResourceId() {
		return String.valueOf(hashCode);
	}

	public static String getResourceParamName() {
		return "rsid";
	}

	public static String getUrlWithResourceId(String url) {
		return String.format("%s?%s=%s", url, getResourceParamName(), getResourceId());
	}
}

Keď už mám definovanú základnú logiku riadenia resourcov, potrebujem aby wicket rozpoznal nový tag parameter a následne ho posunul na spracovanie. To sa vo Wickete realizuje prostredníctvom markup filterov.

public class CustomResourceMarkupFilter extends AbstractMarkupFilter {

	private static final String WICKET_RESOURCE_ATTRIBUTE = "resource";
	private static final String WICKET_RESOURCE_ATTRIBUTE_RESOURCE_ID = "with-resource-id";

	public CustomResourceMarkupFilter() {
		this(null);
	}

	public CustomResourceMarkupFilter(final MarkupResourceStream markupResourceStream) {
		super(markupResourceStream);
	}

	@Override
	protected MarkupElement onComponentTag(ComponentTag tag) throws ParseException {
		if (tag.isClose()) {
			return tag;
		}

		// resource id
		String resourceAttribute = tag.getAttribute(getWicketMessageAttrName());
		if (!StringUtils.isEmpty(resourceAttribute) && WICKET_RESOURCE_ATTRIBUTE_RESOURCE_ID.equals(resourceAttribute)) {
			if ("link".equals(tag.getName()) || "a".equals(tag.getName())) {
				String href = tag.getAttribute("href");
				tag.put("href", addResourceId(href));
			}
			if ("script".equals(tag.getName()) || "img".equals(tag.getName())) {
				String src = tag.getAttribute("src");
				tag.put("src", addResourceId(src));
			}
			tag.remove(getWicketMessageAttrName());
		}
		return tag;
	}

	private String addResourceId(String originalUrl) {
		return ResourceId.getUrlWithResourceId(originalUrl);
	}

	private String getWicketMessageAttrName() {
		return getWicketNamespace() + ':' + WICKET_RESOURCE_ATTRIBUTE;
	}
}

Následne zapojím tento resource filter do markup parsera, ktorý parsuje markup šablóny.

public class CustomMarkupParser extends MarkupParser {

	public CustomMarkupParser(final MarkupResourceStream resource) {
		super(resource);
	}

	public CustomMarkupParser(final String markup) {
		super(markup);
	}

	public CustomMarkupParser(final IXmlPullParser xmlParser, final MarkupResourceStream resource) {
		super(xmlParser, resource);
	}

	@Override
	protected MarkupFilterList initializeMarkupFilters(Markup markup) {
		MarkupFilterList filterList = super.initializeMarkupFilters(markup);

		MarkupResourceStream markupResourceStream = markup.getMarkupResourceStream();
		filterList.add(new CustomResourceMarkupFilter(markupResourceStream));

		return filterList;
	}
}

Takto vytvorrený markup parser nastavím pre celú aplikáciu

public class CustomApplication extends WebApplication {
	@Override
	protected void init() {
		super.init();
		getMarkupSettings().setMarkupFactory(new ItmsMarkupFactory());
	}
}

Po spustení aplikácie si skontrolujeme, ako vyzerá url aktívne riadeného reource.

<link href="/css/main-theme.css?rsid=313388782" rel="stylesheet">

Tento príklad som pre názornosť obmedzil len na jednoduchý scenár použitia. Funkcionalitu generovania resource id je možné realizovať podľa kladených požiadaviek. Keď si resource zabezpečím týmto spôsobom, už sa viac nestane, že zabudnem manuálne aktualizovať jeho verziu.