Migrating Components to Sling Models in AEM

Exadel Digital Experience Team Tech Tips February 24, 2023 7 min read

At some point you might need to migrate more traditional models to Sling models in AEM, particularly if they have some sophisticated default value and custom mapping logic. This AEM development article proposes an effective approach to this task.

Get Your Sling Models in AEM Right

Whilst migrating all components to a Sling Model, we realized that there’s a lot of JS models where:

  • a field has default values if nothing is in the repository
  • a field has its own special mapping depending on a value in the repository

text-heading-model.js:

"use strict";
 
   use(["../utils/PropertyUtils.js", "../utils/compose.js", "../cta/cta.js", "../containers/background/background.js"], function (PropertyUtils, Compose, ctaModel, backgroundModel) {
      var titleSize = PropertyUtils.getProperty('titleSize') || 'h3';
      var titleAlignment = PropertyUtils.getProperty('titleAlignment') || '';
      var titleColor = PropertyUtils.getProperty('titleColor');
      var uppercase = PropertyUtils.getPropertyBoolean('uppercase') ? 'text-uppercase' : '';
 
      if(titleColor == 'default') {
          titleColor = '';
      }
 
      var textHeadingModel = {
          textHeadingTitle: PropertyUtils.getProperty('textHeadingTitle'),
          textHeadingBody: PropertyUtils.getProperty('text'),
          titleSize: titleSize,
          titleClasses: titleAlignment + ' ' + titleColor + ' ' + uppercase,
          hideInMobile: PropertyUtils.getPropertyBoolean('hideInMobile') ? 'hidden-xs' : ''
      };
      return Compose.call(ctaModel, backgroundModel, textHeadingModel);
  });
See more See less

As the first field type issue can be solved by using @Default annotation, then the second type of field migration could have led to various if-else blocks in @PostConstruct method to map the values.

MapValue Annotation

In order to eliminate this repetition of conditional blocks, we introduced an annotation to handle the mappings. Take a look at the custom annotation for handling AEM Sling mapping:

package com.wcm.site.models.mapper;
 
  import java.lang.annotation.ElementType;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.lang.annotation.Target;
  import org.apache.commons.lang3.StringUtils;
  import org.apache.sling.models.annotations.Source;
  import org.apache.sling.models.spi.injectorspecific.InjectAnnotation;
 
  @Target({ElementType.FIELD})
  @Retention(RetentionPolicy.RUNTIME)
  @InjectAnnotation
  @Source(MapValueInjector.NAME)
  public @interface MapValue {
 
      /**
      * define whether strict the mapping is or not
      * for strict = false mapping the repository value itself will be returned if appropriate mapping wasn't found
      * for strict = true the default value will be returned if appropriate mapping wasn't found
      * @return
      */
      boolean strict() default false;
 
      /**
      * define the default value if the property is not set or the appropriate mapping not found for strict mode
      * @return
      */
      String defaultValue() default StringUtils.EMPTY;
 
      Mapping[] mappings() default {};
  }
See more See less

An additional annotation to list AEM Sling mapping

package com.wcm.site.models.mapper;
 
  import java.lang.annotation.ElementType;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.lang.annotation.Target;
 
  @Target({ElementType.PARAMETER})
  @Retention(RetentionPolicy.RUNTIME)
  public @interface Mapping {
 
      String[] from() default {};
 
      String to();
  }
See more See less

The Injector

Now, we need the AEM Sling mapping injector:

package com.wcm.site.models.mapper;
 
  import java.lang.reflect.AnnotatedElement;
  import java.lang.reflect.Type;
  import java.util.Objects;
  import java.util.stream.Stream;
  import javax.inject.Named;
  import org.apache.commons.beanutils.ConvertUtils;
  import org.apache.commons.lang3.StringUtils;
  import org.apache.sling.api.SlingHttpServletRequest;
  import org.apache.sling.api.resource.Resource;
  import org.apache.sling.models.spi.DisposalCallbackRegistry;
  import org.apache.sling.models.spi.Injector;
  import org.osgi.service.component.annotations.Component;
  import org.osgi.service.component.propertytypes.ServiceRanking;
 
  @Component(service = Injector.class)
  @ServiceRanking(4300)
  public class MapValueInjector implements Injector {
 
      public static final String NAME = "map-value-annotation";
 
      @Override
      public String getName() {
          return NAME;
      }
 
      @Override
      public Object getValue(final Object adaptable, final String name, final Type type,
                            final AnnotatedElement element, final DisposalCallbackRegistry callbackRegistry) {
 
          MapValue mapAnnotation = element.getAnnotation(MapValue.class);
          Named named = element.getAnnotation(Named.class);
          if (mapAnnotation == null) {
              return null;
          }
 
          Resource adaptableResource = getResource(adaptable);
          if (adaptableResource == null) {
              return null;
          }
 
          if (!(type instanceof Class<?>)) {
              return null;
          }
          Class<?> fieldClass = (Class<?>) type;
          String fieldName = StringUtils.defaultIfBlank(named != null ? named.value() : null, name);
 
          return getValue(adaptableResource, fieldClass, fieldName, mapAnnotation);
      }
 
      private <T> Object getValue(Resource resource, Class<T> fieldClass, String name, MapValue mapAnnotation) {
          T value = resource.getValueMap().get(name, fieldClass);
          if (value == null) {
              return defaultValue(mapAnnotation, fieldClass);
          }
 
          return Stream.of(mapAnnotation.mappings())
                  .map(mapping -> processMapping(resource, name, fieldClass, mapping))
                  .filter(Objects::nonNull)
                  .findFirst()
                  .orElse(mapAnnotation.strict() ? defaultValue(mapAnnotation, fieldClass) : value);
      }
 
 
      private <T> Object processMapping(Resource resource, String name, Class<T> fieldClass, Mapping mapping) {
          String strValue = resource.getValueMap().get(name, String.class);
          if (StringUtils.equalsAny(strValue, mapping.from())) {
              return ConvertUtils.convert(mapping.to(), fieldClass);
          }
 
          return null;
      }
 
      private Resource getResource(Object adaptable) {
          if (adaptable instanceof SlingHttpServletRequest) {
              return ((SlingHttpServletRequest) adaptable).getResource();
          }
          if (adaptable instanceof Resource) {
              return (Resource) adaptable;
          }
 
          return null;
      }
 
      private <T> Object defaultValue(MapValue annotation, Class<T> clazz) {
          return ConvertUtils.convert(annotation.defaultValue(), clazz);
      }
 
  }
See more See less

A Resulting Component

Now everything is in order for us to rewrite custom mappings, resulting in AEM Sling model mapping done neatly.

TestHeadingModel.java:

package com.wcm.site.models;
 
  import com.wcm.site.models.mapper.MapValue;
  import com.wcm.site.models.mapper.Mapping;
  import javax.annotation.PostConstruct;
  import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
  import javax.inject.Named;
  import org.apache.commons.lang3.StringUtils;
  import org.apache.sling.api.resource.Resource;
  import org.apache.sling.models.annotations.Default;
  import org.apache.sling.models.annotations.DefaultInjectionStrategy;
  import org.apache.sling.models.annotations.Model;
  import org.apache.sling.models.annotations.injectorspecific.Self;
  
  @Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
  public class TextHeadingModel {
  
      @ValueMapValue
      private String textHeadingTitle;
  
      @ValueMapValue
      @Named("text")
      private String textHeadingBody;
  
      @ValueMapValue
      @Default(values = "h3")
      private String titleSize;
  
      @ValueMapValue
      @Default(values = StringUtils.EMPTY)
      private String titleAlignment;
  
      @MapValue(mappings = @Mapping(from = "default", to = StringUtils.EMPTY))
      private String titleColor;
  
      @MapValue(strict = true, mappings = @Mapping(from = "true", to = "text-uppercase"))
      private String uppercase;
  
      @MapValue(strict = true, mappings = @Mapping(from = "true", to = "hidden-xs"))
      private String hideInMobile;
  
      public String getTextHeadingTitle() {
          return textHeadingTitle;
      }
  
      public String getTextHeadingBody() {
          return textHeadingBody;
      }
  
      public String getTitleSize() {
          return titleSize;
      }
  
      public String getHideInMobile() {
          return hideInMobile;
      }
  
  }
See more See less

Master Sling Models in AEM

That’s pretty much it! Instead of countless if-else blocks, we have annotated fields with their mappings. This will improve code readability and simplify potential future updates.

If you are also migrating your code to Sling models, you can use Exadel Authoring Kit Injectors to reduce the boilerplate code and simplify the development process.

Author: Iryna Ason

Was this article useful for you?

Get in the know with our publications, including the latest expert blogs