Przy tworzeniu jakiegokolwiek bardziej złożonego projektu w AEM prędzej czy później pojawia się temat przechowywania i dostępu do różnych danych konfiguracyjnych. Mogą to być zarówno informacje platformowe, klucze API, konfiguracje komponentów, jak i inne parametry.
W projektach dane te zazwyczaj przechowuje się z użyciem jednego lub kilku spośród poniższych mechanizmów:
- Run Mode,
- konfiguracje OSGi,
- właściwości stron (Page Properties),
- niestandardowe strony konfiguracji,
- Sling Context‑Aware Configuration.
Należy pamiętać, że każdy z nich został stworzony do konkretnego celu i zgodnie z dobrymi praktykami nie powinien być używany w sposób niezgodny z przeznaczeniem.
- Run Modes – powinny być wykorzystywane do danych związanych ze środowiskiem, np. konfiguracje autora lub publish.
- Konfiguracje OSGi – najlepiej stosować je do nieedytowalnych przez autorów systemowych konfiguracji, takich jak: schedulery, serwisy, cache, integracje.
- Page Properties – dobrze nadają się do konfiguracji zachowania pojedynczej strony, ale słabo się skalują i są trudne w zarządzaniu przy dużych projektach. Nie powinno się tam przechowywać informacji technicznych.
- Niestandardowe strony konfiguracji – zwykle są elastyczne, ale wymagają dodatkowego rozwoju i dalszego utrzymania.
- Sling Context‑Aware Configuration – w tę opcję zagłębimy się dokładniej w dalszej części artykułu.
Sling Context‑Aware Configuration
Zgodnie z dokumentacją, Context‑Aware Configurations (CAC) to konfiguracje powiązane z konkretnym zasobem lub drzewem zasobów, np.: stroną, stroną główną witryny czy tenantem. Obsługują one:
- dziedziczenie – nie trzeba ustawiać wartości na każdej stronie poddrzewa,
- fallback – wartości są wyszukiwane aż do korzenia konfiguracji.
Domyślna implementacja przechowuje dane pod ścieżką/conf, zapewniając dodatkowe fallbacki. Konfiguracja jest powiązana z poddrzewem treści poprzez właściwość sling:configRef, wskazującą na odpowiedni węzeł konfiguracji. Wszystkie zasoby pod tym poddrzewem próbują znaleźć odpowiednie konfiguracje, przechodząc w górę hierarchii, jeśli to konieczne.
/content
/tenant1
@sling:configRef="/conf/tenant1"
/region1
@sling:configRef="/conf/tenant1/region1"
/site1
@sling:configRef="/conf/tenant1/region1/site1"
/conf
/tenant1
/sling:configs
/x.y.z.MyConfig
@prop1 = 'tenant1"
/region1
/sling:configs
/x.y.z.MyConfig
@prop1 = 'region1"
/site1
/sling:configs
/x.y.z.MyConfig
@prop1 = 'site1'
Dane konfiguracyjne są przechowywane pod węzłem sling:configs, a same węzły konfiguracji są nazwane zgodnie z nazwami klas, np. x.y.z.MyConfig, i zawierają pola, np. prop1.
Jak zobaczymy w dalszej części, mechanizm ten jest bardzo elastyczny.
Interfejsy konfiguracji
@Configuration(label="Configuration", description="Sample configuration")
public @interface MyConfig {
@Property(label="Parameter #1", description="Describe me")
String param1();
}
Są to zwykłe interfejsy z adnotacjami @Configuration i @Property. Możliwe jest również tworzenie konfiguracji zagnieżdżonych oraz tablic:
@Configuration(label="Configuration List", description="Sample list configuration")
public @interface MyConfigList {
@Property(label="Key Value Pairs", description="List of key value pairs")
KeyValue[] keyValues();
@interface KeyValue {
@Property
String key();
@Property
String valye
}
}
Odczyt konfiguracji
Resource contentResource = resourceResolver.getResource("/content/tenant1/region1/site1/page1");
MyConfig config = contentResource.adaptTo(ConfigurationBuilder.class).as(MyConfig.class);
Jak widać, jest to bardzo wygodne rozwiązanie, ponieważ cała logika związana z dostarczaniem właściwego kontekstu, wyszukiwaniem wartości pól, dziedziczeniem oraz mechanizmem fallback jest obsługiwana przez bibliotekę Context-Aware Configuration, bez konieczności pisania jakiegokolwiek własnego kodu. Warto również wspomnieć, że Sling CAC jest częścią standardowej instalacji OOTB AEM.
Co jednak, kiedy mamy specjalne wymagania, a mimo to chcielibyśmy nadal korzystać z CAC? Na szczęście, biblioteka pozwala dostosować ją do naszych potrzeb.
Dostosowanie i adaptacja do potrzeb projektu
Potencjalną wadą przechowywania danych konfiguracyjnych w CAC jest brak przyjaznego, out-of-the-box interfejsu użytkownika do edycji konfiguracji. Obecnie konfigurację można zmieniać jedynie poprzez dostarczanie jej wraz z paczką kodu albo bezpośrednio w narzędziu CRXDE.
Ale co w przypadku, kiedy chcemy, aby autorzy mogli edytować przynajmniej część konfiguracji i ją publikować? Na szczęście da się to zrealizować przy użyciu niewielkiej ilości własnego kodu do edycji i zapisu danych – co ważne, nadal korzystając ze wszystkich zalet CAC przy ich odczycie.
Przyjrzyjmy się scenariuszowi, w którym chcielibyśmy połączyć przechowywanie danych na zwykłych stronach, jako zwykłe komponenty, a jednocześnie odczytywać te wartości w ustandaryzowany sposób.
Załóżmy strukturę:
/content
/tenant1
/region1
/site1
/configuration – configuration page
/jcr:content
/myconfig – configuration component
@prop1 = “value”
gdzie istnieje specjalna strona konfiguracyjna z instancjami komponentów przechowujących poszukiwane przez nas wartości pól.
W pierwszej kolejności musimy zaimplementować interfejs ContextPathStrategy:
@Component(
service = ContextPathStrategy.class
property = {
“service.ranking.integer:1000”
}
}
public class CustomContextPathStrategy implments ContextPathStrategy {
private static final String CONFIG_PAGE = "configuration";
@Override
public Iterator<ContextResource> findContextResource(Resource resource) {
List<ContextResource> contextResources = new ArrayList();
Resource current = resource;
while (current != null) {
if (current.getPath().startsWith("/content/")) {
Resource configurationPage = current.getChild(CONFIG_PAGE);
if(configurationPage != null) {
contextResources.add(new ContextResource(configurationPage, current.getPath()));
}
}
current.getParent();
}
return contextResources.iterator();
}
}
Ta usługa OSGI będzie używana podczas wyszukiwania ścieżki kontekstu. Wysoki ranking zapewnia, że zostanie użyta jako pierwsza. W metodzie findContextResource przeszukujemy hierarchię zasobów w górę do poziomu /content, szukając takiego, który posiada dziecko o nazwie configuration – czyli naszej strony konfiguracyjnej. Jeśli zostanie znaleziona, jest dodawana do wyniku.
Następnie przychodzi czas na użycie tej usługi we własnej implementacji ConfigurationResourceResolvingStrategy. W tym miejscu wprost wykorzystujemy CustomContextPathStrategy, która zwraca wyszukane ścieżki kontekstu.
Następnie iterujemy po nich, szukając komponentu, którego nazwa odpowiada nazwie interfejsu konfiguracji (MyConfig) oraz zgodna jest z zasadami tworzenia nazw komponentów w AEM (metoda getComponentName). Jeśli komponent zostanie znaleziony – metoda go zwraca, a jeśli nie – następuje fallback do kolejnej ścieżki kontekstu i cały scenariusz jest powtarzany.
@Component {
serice = ConfigurationResourceResolvingStrategy.class,
property = {
"serivce.ranking:Integer=1000"
}
}
public class CustomConfigurationResourceResolvingStrategy implements ConfigurationResourceResolvingStrategy {
@Reference(target="(component.name=x.y.z.CustomContextPathStrategy)")
private ContextPathStrategy contextPathStrategy;
@Override
public Resource getResource(Resource resourceResolver, Collection<String> bucketName) {
String className = configName.substring(configName.lastIndex('.') + 1);
return findContextResources(contentResource)
.map(configurationPage -> findChildResource(configurationPage, getComponentName(className)))
.filter(Object::nonNull)
.findFirst()
.orElse(null)
}
private String getComponentName(String className) {
String componentName = className.toLowerCase();
if(componentName.length() > 20) {
return componentName.substring(0, 20);
}
return componentName;
}
private Resource findChildResource(Resource resource, String name) {
if(resource.getName().equals(name)) {
return resource;
}
for(Resource child : resource.getChildren()) {
Resource result = findChildResource(child, name);
if(resoult != null) {
return result;
}
}
return null;
}
...
implmenetation of the rest of the methods
...
}

Podsumowanie
W podobny sposób można obsługiwać kolekcje. Trzeba jedynie pamiętać, że nazwa konfiguracji ma postać x.y.z.MyConfig/jcr:content/keyValues i trzeba to uwzględnić, aby była poprawnie zapisywana jak i odczytywana.
Część /jcr:content pochodzi z wewnętrznej implementacji Granite i w trakcie moich eksperymentów nie udało mi się jej zmienić.
Stworzenie implementacji tych dwóch interfejsów jest wystarczające, aby obsłużyć nasz niestandardowy scenariusz i stanowi świetny przykład elastyczności mechanizmu Sling Context-Aware Customization.
Dalsze rozważania
Istnieje również biblioteka 3rd party, która udostępnia UI oraz własne strategie konfiguracji: Context-Aware Configuration | wcm.io
Zawsze warto także zajrzeć do oficjalnej dokumentacji: Apache Sling :: Apache Sling Context-Aware Configuration
Zostaw komentarz