[JAVA] Comment créer votre propre API headless à l'aide de REST Builder de Liferay (partie 4)

introduction

Cette série de blogs, qui vise à générer votre propre API headless à l'aide des outils REST Builder de Liferay, est enfin terminée!

Dans Partie 1, créez de nouveaux projets et modules, sans tête grâce à une description des composants réutilisables. Nous avons commencé à créer le fichier OpenAPI Yaml qui définit le service. Dans la Partie 2, vous trouverez des problèmes courants liés à l'ajout de chemins et au code généré par REST Builder et à leur résolution. Après avoir parcouru les solutions de contournement, j'ai terminé le fichier OpenAPI Yaml. Dans la Partie 3, examinez tout le code généré, comprenez la construction et où trouver le code d'implémentation. J'ai appris à ajouter.

Dans ce chapitre, qui est le dernier de la série, nous allons créer la couche ServiceBuilder (SB) requise pour la persistance de la valeur, et porter une attention particulière aux parties qui doivent être implémentées pour prendre en charge l'API headless.

Création d'une couche Service Builder

Utilisez Service Builder pour la couche de persistance. Je n'entrerai pas dans tous les détails de ce processus, mais je me concentrerai sur ce que j'ajouterai pour améliorer la commodité de l'API headless.

L'aspect le plus complexe de la partie service est le chemin / vitamins, qui reçoit toutes les" vitamines ", ce qui est le plus facile à ressentir au premier coup d'œil.

Pourquoi est-ce si difficile? En effet, nous devons considérer les points suivants selon le modèle de Liferay:

Pour réaliser tout cela, vous devez vous assurer que l'entité est indexée. Veuillez consulter ici pour la méthode de confirmation.

Le nouvel index prend en charge les autorisations par défaut, vous devez donc ajouter des autorisations à l'entité. Article de référence: https://portal.liferay.dev/docs/7-2/frameworks/-/knowledge_base/f/defining-application-permissions

J'ai nommé le composant «Vitamine», j'ai donc décidé de ne pas utiliser de vitamine dans Service Builder. Sinon, vous devrez inclure le package partout. J'ai décidé d'appeler l'entité «PersistedVitamin» à la place. Cela vous permet de faire la distinction entre la classe DTO utilisée par Headless et l'entité persistante réelle gérée par Service Builder.

Filtre de liste, recherche et support de tri

Le reste de cette section décrit l'ajout de la prise en charge du filtrage, de la recherche et du tri des listes à l'aide des mécanismes pris en charge par Liferay. Cette section peut ne pas s'appliquer si vous ne prenez pas en charge le filtrage, la recherche ou le tri de liste, ou si vous n'avez besoin que de l'assistance pour l'un ou l'autre et n'utilisez pas l'approche de Liferay.

De nombreuses méthodes de liste de Liferay, telles que / v1.0 / message-board-threads / {messageBoardThreadId} / message-board-messages, ont de nombreuses méthodes de liste de Liferay pour prendre en charge la recherche, le filtrage, le tri, la pagination et les restrictions de champ. Des attributs supplémentaires peuvent être fournis dans la requête.

Toute la documentation Liferay sur ces détails:

Certains points non mentionnés dans le document ci-dessus sont des filtres, des tris et des recherches qui vous obligent à utiliser l'index de recherche de l'entité.

Par exemple, une recherche est effectuée en ajoutant un ou plusieurs mots-clés à la requête. Ceux-ci sont saisis dans la requête d'index pour rechercher une correspondance d'entité.

Le filtrage est également géré en ajustant les requêtes de recherche d'index. Pour appliquer un filtre à un ou plusieurs champs d'un composant, ces champs doivent figurer dans l'index de recherche. De plus, les champs décrits dans les autres sections ci-dessous requièrent ʻOData EntityModel`.

Le tri est également géré en ajustant les requêtes de recherche d'index. Pour trier par un ou plusieurs champs dans un composant, ces champs doivent être dans l'index de recherche. De plus, vous devez indexer en utilisant la méthode ʻaddKeywordSortable () de l'interface com.liferay.portal.kernel.search.Document, et ajouter des champs triables à l'implémentation ʻOData EntityModel mentionnée plus tard. besoin de le faire.

En gardant ce qui précède à l'esprit, une attention particulière doit être accordée à la définition de recherche pour les entités personnalisées:

--Utilisez ModelDocumentContributor pour ajouter du texte et des mots-clés importants pour obtenir les bons résultats de recherche. --Utilisez ModelDocumentContributor pour ajouter un champ prenant en charge le filtrage. --Utilisez ModelDocumentContributor pour ajouter un champ de mot-clé triable.

Mise en œuvre de la méthode VitaminResourceImpl

Après avoir créé la couche Service Builder et corrigé la dépendance headless-vitamins-impl, l'étape suivante consiste à commencer à implémenter la méthode.

Implémentation de deleteVitamin ()

Commençons par une simple méthode deleteVitamin (). VitaminResourceImpl étend la méthode à partir de la classe de base (avec toutes les annotations) et appelle la couche de service:

@Override
public void deleteVitamin(@NotNull String vitaminId) throws Exception {
  // super easy case, just pass through to the service layer.
  _persistedVitaminService.deletePersistedVitamin(vitaminId);
}

Nous vous recommandons d'utiliser uniquement des services distants, et non des services locaux, pour gérer la persistance des entités. Pourquoi? Vérifier si un utilisateur a l'autorisation de supprimer un enregistrement «vitaminé» n'est que votre dernière ligne de défense.

Vous pouvez utiliser l'étendue OAuth2 pour effectuer des contrôles et bloquer l'activité, mais il est difficile pour un administrateur de configurer correctement l'étendue OAuth2, et même si je suis moi-même administrateur, je peux obtenir l'étendue correctement à chaque fois. Je ne pense pas.

En utilisant un service distant avec vérification des autorisations d'accès, vous n'avez pas à vous soucier de la cohérence de l'étendue. Même si l'administrateur (I) désactive l'étendue OAuth2, le service distant bloquera l'opération à moins que l'utilisateur ne dispose des autorisations appropriées.

Processus de conversion

Avant de discuter plus en détail de certaines des méthodes d'implémentation, nous devons discuter de la conversion de l'entité backend ServiceBuilder vers le composant headless renvoyé.

À l'heure actuelle, Liferay n'a pas établi de norme pour gérer les conversions d'entité en composant. Le module source Liferay headless-delivery-impl effectue la conversion dans un sens, tandis que le module headless-admin-user-impl gère la conversion différemment.

Pour plus de commodité, voici une technique basée sur la technique headless-admin-user-impl. Il peut y avoir des méthodes différentes et plus efficaces, ou vous pouvez préférer la méthode headless-delivery-impl. Liferay pourrait également proposer un moyen standard de prendre en charge les conversions dans la prochaine version.

Bien qu'il dise qu'il doit être converti, il n'est lié à aucune méthode particulière. Liferay peut sortir mieux, mais c'est à vous de vous adapter à la nouvelle méthode ou de prendre la vôtre.

Par conséquent, vous devez être capable de convertir le composant «PersistedVitamin» en un composant Vitamin pour le renvoyer dans le cadre d'une définition d'API headless. Créez la méthode _toVitamin () dans la classe VitaminResourceImpl:

protected Vitamin _toVitamin(PersistedVitamin pv) throws Exception {
  return new Vitamin() {{
    creator = CreatorUtil.toCreator(_portal, _userLocalService.getUser(pv.getUserId()));
    articleId = pv.getArticleId();
    group = pv.getGroupName();
    description = pv.getDescription();
    id = pv.getSurrogateId();
    name = pv.getName();
    type = _toVitaminType(pv.getType());
    attributes = ListUtil.toArray(pv.getAttributes(), VALUE_ACCESSOR);
    chemicalNames = ListUtil.toArray(pv.getChemicalNames(), VALUE_ACCESSOR);
    properties = ListUtil.toArray(pv.getProperties(), VALUE_ACCESSOR);
    risks = ListUtil.toArray(pv.getRisks(), VALUE_ACCESSOR);
    symptoms = ListUtil.toArray(pv.getSymptoms(), VALUE_ACCESSOR);
  }};
}

Tout d'abord, je dois m'excuser pour l'utilisation de l'instanciation à double accolade ... Je le reconnais aussi comme anti-pattern. Mais mon objectif était de suivre la "méthode Liferay" présentée dans le module headless-admin-user-impl, qui était le modèle utilisé par Liferay. Liferay n'utilise pas très souvent le modèle Builder, donc je pense que l'instanciation à double accolade est utilisée à la place.

Compte tenu de mon goût, je suis également les modèles Builder et Fluent pour simplifier la création d'objets. Après tout, Intellij me permet de créer facilement une classe Builder pour moi.

Cette méthode est implémentée par la classe externe CreatorUtil (copiée à partir du code Liferay), la méthode _toVitaminType () qui convertit le code entier interne en un type d'énumération de composant et la méthode ListUtil toArray (). Utilisez VALUE_ACCESSOR pour traiter certains objets internes dans un tableau String.

En bref, cette méthode peut gérer les transformations qui doivent être effectuées dans l'implémentation réelle de la méthode.

Implémentation de getVitamin ()

Regardons une autre méthode simple getVitamin (). Cette méthode renvoie une seule entité avec «vitaminId».

@Override
public Vitamin getVitamin(@NotNull String vitaminId) throws Exception {
  // fetch the entity class...
  PersistedVitamin pv = _persistedVitaminService.getPersistedVitamin(vitaminId);

  return _toVitamin(pv);
}

Ici, nous obtenons l'instance PersistedVitamin de la couche de service, mais passons l'objet obtenu à la méthode _toVitamin () pour le convertir.

Implémentations de postVitamin (), patchVitamin (), et putVitamin ()

Je pense que vous êtes déjà fatigué de voir les modèles, alors jetons un coup d'œil à eux tous ensemble.

postVitamin () est une méthode POST pour / vitamins et représente la création d'une nouvelle entité.

patchVitamin () est la méthode PATCH de / vitamins / {vitaminId}, qui représente l'application de correctifs à une entité existante (en laissant d'autres propriétés existantes, seulement la valeur spécifiée pour l'objet d'entrée). Changer).

putVitamin () est la méthode PUT de / vitamins / {vitaminId}, qui représente le remplacement d'une entité existante, remplaçant toutes les valeurs persistantes par les valeurs passées, même si le champ est nul ou vide.

Depuis que nous avons créé la couche ServiceBuilder et l'avons personnalisée pour ces points d'entrée, l'implémentation dans la classe VitaminResourceImpl semble très légère.

@Override
public Vitamin postVitamin(Vitamin v) throws Exception {
  PersistedVitamin pv = _persistedVitaminService.addPersistedVitamin(
      v.getId(), v.getName(), v.getGroup(), v.getDescription(), _toTypeCode(v.getType()), v.getArticleId(), v.getChemicalNames(),
      v.getProperties(), v.getAttributes(), v.getSymptoms(), v.getRisks(), _getServiceContext());

  return _toVitamin(pv);
}

@Override
public Vitamin patchVitamin(@NotNull String vitaminId, Vitamin v) throws Exception {
  PersistedVitamin pv = _persistedVitaminService.patchPersistedVitamin(vitaminId,
      v.getId(), v.getName(), v.getGroup(), v.getDescription(), _toTypeCode(v.getType()), v.getArticleId(), v.getChemicalNames(),
      v.getProperties(), v.getAttributes(), v.getSymptoms(), v.getRisks(), _getServiceContext());

  return _toVitamin(pv);
}

@Override
public Vitamin putVitamin(@NotNull String vitaminId, Vitamin v) throws Exception {
  PersistedVitamin pv = _persistedVitaminService.updatePersistedVitamin(vitaminId,
      v.getId(), v.getName(), v.getGroup(), v.getDescription(), _toTypeCode(v.getType()), v.getArticleId(), v.getChemicalNames(),
      v.getProperties(), v.getAttributes(), v.getSymptoms(), v.getRisks(), _getServiceContext());

  return _toVitamin(pv);
}

Comme vous pouvez le voir, il est très léger.

Vous avez besoin de ServiceContext pour accéder à la couche de service. Liferay fournit com.liferay.headless.common.spi.service.context.ServiceContextUtil. Il ne dispose que des méthodes nécessaires pour créer un ServiceContext. Ceci est un démarreur de contexte, ajoutez simplement des informations supplémentaires telles que l'ID d'entreprise et l'ID utilisateur actuel. J'ai donc enveloppé tout cela dans la méthode _getServiceContext (). Les futures versions de REST Builder recevront de nouvelles variables de contexte pour faciliter l'obtention d'un ServiceContext valide.

Toutes mes méthodes ServiceBuilder passent et utilisent des paramètres étendus que tout le monde connaît sur ServiceBuilder. L'instance PersistedValue renvoyée par l'appel de méthode est passée à _toVitamin () pour conversion et renvoyée.

Ce qui précède est une solution simple. Nous devons également expliquer la méthode getVitaminsPage (), mais avant cela, nous devons expliquer ʻEntityModels`.

EntityModels

Plus tôt, nous avons expliqué comment Liferay peut utiliser les index de recherche pour prendre en charge le filtrage, la recherche et le tri des listes. Nous avons également expliqué que les champs qui peuvent être utilisés pour le filtrage et le tri doivent faire partie de la définition ʻEntityModel du composant. Les champs des composants qui ne font pas partie de ʻEntityModel ne peuvent pas être filtrés ou triés.

Comme effet secondaire supplémentaire, ʻEntityModel` expose ces champs de l'index de recherche pour le filtrage et le tri, vous n'avez donc pas besoin de connecter ces champs aux champs de composant.

Par exemple, dans la définition ʻEntityModel, vous pouvez ajouter une entrée pour creatorIdqui filtre l'ID utilisateur de l'index de recherche. La définition du composant peut contenir un champCreator au lieu d'un champ creatorId, mais comme creatorId fait partie de ʻEntityModel, il peut être utilisé à la fois pour le filtrage et le tri.

Par conséquent, nous devons construire un ʻEntityModel qui définit les champs qui prennent en charge à la fois le filtrage et le tri. Utilisez l'utilitaire Liferay existant pour assembler la classe ʻEntityModel:

public class VitaminEntityModel implements EntityModel {
  public VitaminEntityModel() {
    _entityFieldsMap = Stream.of(
        // chemicalNames is a string array of the chemical names of the vitamins/minerals
        new CollectionEntityField(
            new StringEntityField(
                "chemicalNames", locale -> Field.getSortableFieldName("chemicalNames"))),
        
        // we'll support filtering based upon user creator id.
        new IntegerEntityField("creatorId", locale -> Field.USER_ID),
        
        // sorting/filtering on name is okay too
        new StringEntityField(
            "name", locale -> Field.getSortableFieldName(Field.NAME)),
        
        // as is sorting/filtering on the vitamin group
        new StringEntityField(
            "group", locale -> Field.getSortableFieldName("vitaminGroup")),
        
        // and the type (vitamin, mineral, other).
        new StringEntityField(
            "type", locale -> Field.getSortableFieldName("vType"))
    ).collect(
        Collectors.toMap(EntityField::getName, Function.identity())
    );
  }

  @Override
  public Map<String, EntityField> getEntityFieldsMap() {
    return _entityFieldsMap;
  }

  private final Map<String, EntityField> _entityFieldsMap;
}

Le nom du champ provient du nom utilisé dans la classe PersistedVitaminModelDocumentContributor de la couche de service pour ajouter la valeur du champ.

Inclus des définitions pour ChemicalNames, Field.USER_ID, Field.NAME, vitaminGroup et vType Fields de l'index de recherche. Parmi les définitions, le champ creatorId utilisé par le filtre n'existe pas en tant que champ dans la définition du composant de vitamine.

D'autres champs qui font partie du composant Vitamine donnent l'impression que vous n'avez pas besoin d'autoriser le reste du tri ou du filtrage. Ce type de décision est généralement déterminé par les exigences.

Liferay stocke ces classes dans un package interne, le package ʻodata.entity.v1_0, donc le fichier à mettre dans mon cas est com.dnebinger.headless.delivery.internal.odata.entity.v1_0`. J'attends.

Maintenant que la classe est prête, nous devons également décorer la classe VitaminResourceImpl pour nous assurer que nous pouvons fournir correctement ʻEntityModel`.

Les changements requis sont:

Mon VitaminEntityModel est très simple et pas très dynamique, donc la mise en œuvre ressemble à ceci:

public class VitaminResourceImpl extends BaseVitaminResourceImpl 
    implements EntityModelResource {

  private VitaminEntityModel _vitaminEntityModel = new VitaminEntityModel();

  @Override
  public EntityModel getEntityModel(MultivaluedMap multivaluedMap) throws Exception {
    return _vitaminEntityModel;
  }

Veuillez noter qu'il ne s'agit pas d'une implémentation courante. La classe d'implémentation des ressources de composants de Liferay a une génération de ʻEntityModelbeaucoup plus complexe et dynamique, ce qui est dû à la complexité des entités associées (par exemple,StructuredContent est JournalArticle, DDM Structure. , Une collection de modèles`).

Alors ne copiez pas et exécutez simplement la méthode. Cela peut fonctionner dans votre cas, mais pas dans d'autres cas. Pour des scénarios plus complexes, consultez l'implémentation Liferay de la classe ʻEntityModel et la méthode getEntityModel () `de l'implémentation des ressources du composant.

Implémentation de getVitaminsPage ()

C'est probablement la méthode de mise en œuvre la plus compliquée. Ce n'est pas difficile en soi, cela dépend de bien d'autres choses.

La fonction de traitement de la liste Liferay provient ici de l'index de recherche et non de la base de données. Par conséquent, l'entité doit être indexée.

Il s'agit également d'une méthode qui prend en charge les paramètres de filtrage, de recherche et de tri, et l'entité doit être indexée. Et comme nous l'avons vu précédemment, le filtrage et le tri dépendent également de la classe ʻEntityModel`.

Enfin, comme nous appelons la méthode Liferay, l'implémentation elle-même est assez opaque et incontrôlable. Le résultat final est:

public Page<Vitamin> getVitaminsPage(String search, Filter filter, Pagination pagination, Sort[] sorts) throws Exception {
  return SearchUtil.search(
    booleanQuery -> {
      // does nothing, we just need the UnsafeConsumer<BooleanQuery, Exception> method
    },
    filter, PersistedVitamin.class, search, pagination,
    queryConfig -> queryConfig.setSelectedFieldNames(
      Field.ENTRY_CLASS_PK),
    searchContext -> searchContext.setCompanyId(contextCompany.getCompanyId()),
    document -> _toVitamin(
      _persistedVitaminService.getPersistedVitamin(
        GetterUtil.getLong(document.get(Field.ENTRY_CLASS_PK)))),
    sorts);
}

Nous utilisons la méthode SearchUtil.search (), qui connaît toutes les façons de le faire.

Le premier argument est la classe ʻUnsafeConsumer, qui est essentiellement responsable du réglage fin de la booleanQueryselon les besoins de l'entité. Je n'en avais pas besoin ici, mais le module de livraison sans tête de Liferay a un exemple. La version deStructuredContentqui recherche les articles par ID de site ajoute l'ID de site comme argument de requête. Le paramètreflatten` affine la requête pour rechercher des filtres spécifiques, ceux de ces types.

Les arguments de filtre, de recherche et de pagination obtenus à partir du calque sans tête sont transmis tels quels. Les résultats sont appliqués à la requête Bourian, les résultats sont filtrés et recherchés, et les nations de page donnent les résultats équivalents à la page.

queryConfig renvoie uniquement la valeur de la clé primaire et ne demande aucune autre donnée de champ. Vous avez besoin de l'entité ServiceBuilder car vous ne convertissez pas à partir de l'index de recherche Document.

L'avant-dernier argument est une autre ʻUnsafeFunctionqui applique la conversion de type document-composant. Cette implémentation utilise la valeur de clé primaire extraite du document pour récupérer l'instancePersistedVitamin et le PersistedVitamin est passé à _toVitamin () `pour gérer la conversion finale.

Le travail restant

Vous avez fait tout le codage, mais vous n'avez pas terminé.

Réexécutez la commande buildREST. Maintenant que nous avons ajouté les méthodes à la méthode VitaminResourceImpl, nous aimerions avoir des cas de test qui peuvent leur être appliqués.

Ensuite, vous devez créer et déployer le module pour nettoyer toutes les références ouvertes, les problèmes de déploiement, etc. Déployez les modules «vitamins-api» et «vitamins-service» sur la couche ServiceBuilder et les modules «vitamins-headless-api» et «vitamins-headless-impl» sur la couche Headless.

Quand ils sont prêts, vous devez les déposer dans le module headless-vitamins-test et exécuter tous les cas de test (si vous manquez, vous pouvez les recréer aussi).

Une fois que vous êtes prêt, vous souhaiterez peut-être publier l'API Headless sur SwaggerHub pour que d'autres puissent l'utiliser.

Le fichier Yaml créé pour REST Builder n'est pas utilisé. À la place, dans votre navigateur [http: // localhost: 8080 / o / headless-vitamins / v1.0 / openapi.yaml](http: // localhost: 8080 / o / headless-vitamins / v1.0 / openapi.yaml] ) Est spécifié et le fichier est utilisé pour la transmission. Toutes les pièces requises sont placées et des composants supplémentaires tels que le type «PageVitamin» sont ajoutés.

Résumé

Créez un nouvel espace de travail et un module de validation headless dans Partie 1 et utilisez OpenAPI Yaml pour REST Builder pour enfin générer du code J'ai commencé le fichier.

Dans Partie 2, nous avons ajouté la définition de chemin et complété le fichier REST Builder OpenAPI Yaml. Face aux erreurs de construction de REST Builder, j'ai compris certaines des erreurs de format courantes susceptibles de provoquer des erreurs de construction, je les ai corrigées et j'ai utilisé REST Builder pour générer du code avec succès.

Dans la Partie 3, nous avons examiné tout le code généré par tous les modules et montré où les modifications seraient apportées.

Dans la partie 4 (ce chapitre), nous allons créer une couche Service Builder pour prendre en charge les autorisations de ressources (pour vérifier les autorisations sur les services distants) et l'indexation d'entités (capacités de filtrage / recherche / tri de liste d'infrastructure sans tête de Liferay). À faire) inclus. Ensuite, j'ai expliqué comment gérer la conversion entité-en-composant en vidant la méthode VitaminResourceImpl, et la classe ʻEntityModel` nécessaire pour faciliter le filtrage et le tri.

Nous avons tout testé et probablement publié l'API sur SwaggerHub pour que tout le monde puisse en profiter. Ça a été un long chemin, mais c'était vraiment intéressant pour moi. J'espère que ça vous plait.

Encore une fois, voici le référentiel de cette série de blogs: https://github.com/dnebing/vitamins

Recommended Posts

Comment créer votre propre API headless à l'aide de REST Builder de Liferay (partie 3)
Comment créer votre propre API headless à l'aide de REST Builder de Liferay (partie 2)
Comment créer votre propre API headless à l'aide de REST Builder de Liferay (partie 4)
Comment créer votre propre API headless à l'aide de REST Builder de Liferay (partie 1)
Comment créer votre propre contrôleur correspondant à / error avec Spring Boot
Comment créer votre propre annotation en Java et obtenir la valeur
Créons une API REST à l'aide de WildFly Swarm.
[Rails] Comment créer un graphique à l'aide de lazy_high_charts
Comment implémenter le verrouillage optimiste dans l'API REST
Comment créer des données de catégorie hiérarchique à l'aide de l'ascendance
Comment lire votre propre fichier YAML (*****. Yml) en Java
[Forge] Comment enregistrer votre propre Entité et Entité Render dans 1.13.2
Comment déployer jQuery dans les applications Rails à l'aide de Webpacker
Comment créer un portlet de générateur de services dans Liferay 7 / DXP
Comment lire un fichier MIDI à l'aide de l'API Java Sound
Comment utiliser l'API Chain
Comment utiliser @Builder (Lombok)
Créez vos propres annotations Java
Introduction à l'API EHRbase 2-REST
Comment créer une méthode
Comment autoriser à l'aide de graphql-ruby
Comment créer un fichier jar et un fichier war à l'aide de la commande jar
[Rails 6] Comment créer un écran de saisie de formulaire dynamique à l'aide de cocoon
Un moyen simple de créer une classe de mappage lors de l'utilisation de l'API