Java tutorial
package nc.noumea.mairie.appock.services.impl; /*- * #%L * Logiciel de Gestion des approvisionnements et des stocks des fournitures administratives de la Mairie de Nouma * %% * Copyright (C) 2017 Mairie de Nouma, Nouvelle-Caldonie * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ import nc.noumea.mairie.appock.core.security.AppUser; import nc.noumea.mairie.appock.core.utility.AppockMail; import nc.noumea.mairie.appock.core.utility.DateUtil; import nc.noumea.mairie.appock.dto.ArticleSpecifique; import nc.noumea.mairie.appock.entity.*; import nc.noumea.mairie.appock.entity.CommandeService; import nc.noumea.mairie.appock.entity.Service; import nc.noumea.mairie.appock.enums.Role; import nc.noumea.mairie.appock.repositories.AppUserRepository; import nc.noumea.mairie.appock.services.*; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.util.CollectionUtils; import javax.activation.DataHandler; import javax.annotation.PostConstruct; import javax.mail.*; import javax.mail.internet.*; import javax.mail.util.ByteArrayDataSource; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.file.Files; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; /** * Service de gestion des mails. */ @org.springframework.stereotype.Service("mailService") @Scope(value = "singleton", proxyMode = ScopedProxyMode.TARGET_CLASS) public class MailServiceImpl implements MailService { private static Logger log = LoggerFactory.getLogger(MailServiceImpl.class); @Autowired(required = false) Session mailServer; @Autowired(required = true) Boolean mailServerModeProduction; @Autowired ConfigService configService; @Autowired AuthHelper authHelper; @Autowired AppUserService appUserService; @Autowired AppUserRepository appUserRepository; @Autowired CommandeServiceService commandeServiceService; @Autowired ArticleCatalogueService articleCatalogueService; @Autowired CatalogueService catalogueService; @Autowired ServiceService serviceService; @PostConstruct public void initialize() { // #7763 log.warn("mailServerModeProduction = " + mailServerModeProduction + " : " + // (mailServerModeProduction ? "tous les mails vont partir vers les VRAIS destinataires" : "les mails vont partir vers la PERSONNE CONNECTEE")); } @Override public Message sendMail(AppockMail appockMail) throws MessagingException { if (CollectionUtils.isEmpty(appockMail.getListeDestinataire())) { log.warn("Mail non envoy, liste des destinataires vide : " + appockMail); return null; } MimeMessage msg = new MimeMessage(mailServer); msg.setFrom(appockMail.getFrom() != null ? appockMail.getFrom() : defaultFrom()); String prefixeSujet = isModeProduction() ? "" : "[TEST APPOCK] "; msg.setSubject(prefixeSujet + appockMail.getSujet(), configService.getEncodageSujetEmail()); StringBuilder infoAdditionnelle = new StringBuilder(); for (InternetAddress adresse : appockMail.getListeDestinataire()) { if (isModeProduction()) { // on est en production, envoi rel de mail au destinataire msg.addRecipient(Message.RecipientType.TO, adresse); } else { // en test, on envoie le mail la personne connecte InternetAddress adresseTesteur = getAdresseMailDestinataireTesteur(); if (adresseTesteur != null) { msg.addRecipient(Message.RecipientType.TO, adresseTesteur); } List<String> listeAdresseMail = new ArrayList<>(); for (InternetAddress internetAddress : appockMail.getListeDestinataire()) { listeAdresseMail.add(internetAddress.getAddress()); } infoAdditionnelle.append("En production cet email serait envoy vers ") .append(StringUtils.join(listeAdresseMail, ", ")).append("<br/><hr/>"); break; } } String contenuFinal = ""; if (!StringUtils.isBlank(infoAdditionnelle.toString())) { contenuFinal += "<font face=\"arial\" >" + infoAdditionnelle.toString() + "</font>"; } contenuFinal += "<font face=\"arial\" >" + appockMail.getContenu() + "<br/><br/></font>" + configService.getPiedDeMail(); gereBonLivraisonDansMail(appockMail, msg, contenuFinal); if (!ArrayUtils.isEmpty(msg.getAllRecipients())) { Transport.send(msg); } return msg; } private void gereBonLivraisonDansMail(AppockMail appockMail, MimeMessage msg, String contenuFinal) throws MessagingException { if (appockMail.getBonLivraison() == null) { msg.setContent(contenuFinal, "text/html; charset=utf-8"); } else { BonLivraison bonLivraison = appockMail.getBonLivraison(); Multipart multipart = new MimeMultipart(); BodyPart attachmentBodyPart = new MimeBodyPart(); File file = commandeServiceService.getFileBonLivraison(appockMail.getBonLivraison()); ByteArrayDataSource bds = null; try { bds = new ByteArrayDataSource(Files.readAllBytes(file.toPath()), bonLivraison.getMimeType().getLibelle()); } catch (IOException e) { e.printStackTrace(); } attachmentBodyPart.setDataHandler(new DataHandler(bds)); attachmentBodyPart.setFileName(bonLivraison.getNomFichier()); multipart.addBodyPart(attachmentBodyPart); BodyPart htmlBodyPart = new MimeBodyPart(); htmlBodyPart.setContent(contenuFinal, "text/html; charset=utf-8"); multipart.addBodyPart(htmlBodyPart); msg.setContent(multipart); } } /** * @return true si on est en mode production : les mails doivent partir vers les vrais destinataires, false en mode "test" (ou recette) : les mails partent * vers la personne connecte */ private boolean isModeProduction() { if (mailServerModeProduction == null) { log.error("mailServerModeProduction n'est pas dfini !"); return false; } log.debug("mail mode production = " + mailServerModeProduction); return mailServerModeProduction; } /** * @return l'adresse mail du testeur (en mode non production), null si la personne connecte n'a pas d'email renseign ou s'il n'y a personne de connect * (ex : cas d'un job quartz) * @throws AddressException */ private InternetAddress getAdresseMailDestinataireTesteur() throws AddressException { AppUser currentUser = authHelper.getCurrentUser(); if (currentUser == null) { return null; // pas de personne connecte => pas de destinataire testeur } String email = currentUser.getEmail(); return StringUtils.isNotBlank(email) ? new InternetAddress(email) : null; } private InternetAddress defaultFrom() throws AddressException { return new InternetAddress(configService.getDefaultFromEmailAddress()); } @Override public void sendMailSuiteDesactivationArticleCataloguePanier(ArticleCatalogue articleCatalogue, List<AppUser> listeAppUser) throws UnsupportedEncodingException, MessagingException { AppockMail appockMail = new AppockMail(); appockMail.addDestinataire(listeAppUser); appockMail.setSujet("[APPOCK] Article retir de votre panier"); appockMail.setContenu("" + "Bonjour,<br/><br/>" + "L'article '" + articleCatalogue.getLibelle() + "' ( " + articleCatalogue.getReference() + ") a t dsactiv dans le catalogue. Cet article se trouvait dans le panier de votre service, celui-ci a t retir automatiquement de votre panier."); sendMail(appockMail); } @Override public void sendMailSuiteDesactivationArticleCatalogueDemande(ArticleCatalogue articleCatalogue, Demande demande, List<AppUser> listeAppUser) throws UnsupportedEncodingException, MessagingException { AppockMail appockMail = new AppockMail(); appockMail.addDestinataire(listeAppUser); appockMail.setSujet("[APPOCK] Article dsactiv prsent dans la demande N " + demande.getNumero()); appockMail.setContenu("" + "Bonjour,<br/><br/>" + "L'article '" + articleCatalogue.getLibelle() + "' ( " + articleCatalogue.getReference() + ") a t dsactiv dans le catalogue. Cet article se trouvait dans la demande N " + demande.getNumero() + " de votre service.<br/>Cet article ne pourra pas tre command, merci de le remplacer par un autre article."); sendMail(appockMail); } @Override public void sendMailSuiteTransmissionDemande(Demande demande) throws MessagingException, UnsupportedEncodingException { AppockMail appockMail = new AppockMail(); appockMail.addDestinataire(appUserService.findAllWithRole(Role.ROLE_REFERENT_ACHAT, true)); appockMail.setSujet("[APPOCK] Nouvelle demande traiter - N " + demande.getNumero()); appockMail.setContenu( demande.getTransmetUser() + " du service " + demande.getService().getLibellePoleDirectionService() + " vient de transmettre une nouvelle demande d'approvisionnement N " + demande.getNumero() + ".<br/>Merci de procder au traitement de cette demande."); sendMail(appockMail); } @Override public void sendMailSuiteTraitementDemande(Demande demande) throws MessagingException, UnsupportedEncodingException { AppockMail appockMail = initAppockMailAvecReferentService(demande); if (appockMail == null) return; appockMail.setSujet( "[APPOCK] Traitement de votre demande N " + demande.getNumero() + " par la section achat"); appockMail.setContenu( "" + "Bonjour,<br/><br/>" + demande.getTraitementUser() + " vient de traiter votre demande N " + demande.getNumero() + ". Voici ci-dessous le rapport de ce traitement.<br/><br/>" + construiTableauHtmlRapportTraitementDemande(demande)); sendMail(appockMail); } @Override public void sendMailSuiteAnnulationDemande(Demande demande) throws MessagingException, UnsupportedEncodingException { AppockMail appockMail = initAppockMailAvecReferentService(demande); if (appockMail == null) return; appockMail.setSujet( "[APPOCK] Annulation de votre demande N " + demande.getNumero() + " par la section achat"); appockMail.setContenu("" + "Bonjour,<br/><br/>La section achat vient d'annuler votre demande."); sendMail(appockMail); } @Override public void sendMailSuiteMiseAJourStock(List<Object[]> listeNouveauArticleStockAvecQuantite, List<Object[]> listeExistantArticleStockAvecQuantite, CommandeService commandeService) throws MessagingException, UnsupportedEncodingException { AppockMail appockMail = initAppockMailAvecReferentService(commandeService); AppUser appUser = authHelper.getCurrentUser(); if (appockMail == null) return; appockMail.setSujet( "[APPOCK] Mise jour du stock de votre service suite la rception de la commande N " + commandeService.getCommande().getNumero() + " par le service achat"); appockMail.setContenu("" + "Bonjour,<br/><br/>" + appUser.getNomComplet() + " vient de valider la livraison de votre demande. Voici ci-dessous les mises jour de votre stock en fonction de cette livraison :<br/><br/>" + construiTableauHtmlRapportStock(listeNouveauArticleStockAvecQuantite, listeExistantArticleStockAvecQuantite)); sendMail(appockMail); } private AppockMail initAppockMailAvecReferentService(Demande demande) throws UnsupportedEncodingException { return initAppockMailAvecReferentService(demande.getTransmetLogin()); } private AppockMail initAppockMailAvecReferentService(CommandeService commandeService) throws UnsupportedEncodingException { return initAppockMailAvecReferentService(commandeService.getReceptionLogin()); } private AppockMail initAppockMailAvecReferentService(String loginReferent) throws UnsupportedEncodingException { AppockMail appockMail = new AppockMail(); AppUser appUser = appUserService.findByLogin(loginReferent); if (appUser == null) { return null; } if (!appUser.isTitulaire()) { // Si ce n'est pas le titulaire, on envoie aussi au titulaire AppUser appUserTitulaire = appUserService.findTitulaireByService(appUser.getService()); if (appUserTitulaire != null) { appockMail.addDestinataire(appUserTitulaire); } } appockMail.addDestinataire(appUser); return appockMail; } private String construiTableauHtmlRapportTraitementDemande(Demande demande) { StringBuilder sb = new StringBuilder(""); sb.append("<table style=\"border-collapse: collapse;\">"); sb.append("<tr bgcolor=\"#7FBFFF\">"); sb.append(createTh("Rfrence")); sb.append(createTh("Libell")); sb.append(createTh("Demand")); sb.append(createTh("Valid")); sb.append(createTh("Colisage")); sb.append(createTh("Quantit relle")); sb.append(createTh("Action section achat")); sb.append("</tr>"); for (ArticleDemande articleDemande : demande.getListeArticleDemande()) { String bgColor = null; if (articleDemande.isValide()) { if (!articleDemande.getQuantite().equals(articleDemande.getQuantiteCommande())) { bgColor = "#FAAC58"; } } if (articleDemande.isAjoutReferentAchat()) { bgColor = "#0D8781"; } if (articleDemande.isRejete()) { bgColor = "#FA5858"; } if (StringUtils.isNotBlank(bgColor)) { sb.append("<tr bgcolor=\"").append(bgColor).append("\">"); } else { sb.append("<tr>"); } sb.append(createTd(articleDemande.getArticleCatalogue().getReference(), false)); sb.append(createTd(articleDemande.getArticleCatalogue().getLibelle(), false)); sb.append( createTd(articleDemande.isAjoutReferentAchat() ? articleDemande.getQuantiteCommande().toString() : articleDemande.getQuantite().toString(), true)); sb.append(createTd(articleDemande.isValide() ? articleDemande.getQuantiteCommande().toString() : "0", true)); sb.append(createTd(articleDemande.getArticleCatalogue().getLibelleColisage(), false)); sb.append(createTd( articleDemande.isValide() ? articleDemande.getQuantiteReelCommande().toString() : "0", true)); String libelleEtat = articleDemande.getEtatArticleDemande().getLibelle(); if (articleDemande.isAjoutReferentAchat()) { libelleEtat = "Ajout"; } sb.append(createTd(libelleEtat, true)); sb.append("<tr>"); } sb.append("</table>"); return sb.toString(); } private String createTh(String value) { return "<th style=\"border: 1px solid black;\">" + value + "</th>"; } private String createTd(String value, boolean center) { return "<td style=\"border: 1px solid black;\"" + (center ? " align=\"center\"" : "") + ">" + value + "</td>"; } @Override public void sendMailSuiteReceptionCommandeService(CommandeService commandeService) throws MessagingException, UnsupportedEncodingException { String libelleService = commandeService.getService().getLibellePoleDirectionService(); String numeroCommande = commandeService.getCommande().getNumero(); AppockMail appockMail = new AppockMail(); appockMail.addDestinataire(appUserService.findAllWithRole(Role.ROLE_REFERENT_ACHAT, true)); appockMail.setSujet( "[APPOCK] Rception de la commande N " + numeroCommande + " par le service " + libelleService); String contenu = "Bonjour,<br/><br/>" + commandeService.getReceptionUser() + " du service " + libelleService + " vient de rceptionner la commande N " + numeroCommande + ". Voici ci-dessous le rapport de rception :<br/><br/>" + construiTableauHtmlRapportReceptionCommandeService(commandeService); appockMail.setBonLivraison(commandeService.getBonLivraison()); if (StringUtils.isNotBlank(commandeService.getCommentaire())) { contenu += "<br/>Le rfrent service vous joint le commentaire suivant :<br/>\"<i>" + commandeService.getCommentaire() + "\"</i><br/>"; } contenu += "<br/>Veuillez trouver en pice jointe le bordereau de livraison."; appockMail.setContenu(contenu); sendMail(appockMail); } private String construiTableauHtmlRapportReceptionCommandeService(CommandeService commandeService) { StringBuilder sb = new StringBuilder(""); sb.append("<table style=\"border-collapse: collapse;\">"); sb.append("<tr bgcolor=\"#7FBFFF\">"); sb.append(createTh("Rfrence")); sb.append(createTh("Libell")); sb.append(createTh("Quantit reue")); sb.append(createTh("Quantit commande")); sb.append(createTh("Colisage")); sb.append(createTh("Quantit relle reue")); sb.append(createTh("Quantit relle commande")); sb.append("</tr>"); for (ArticleCommande articleCommande : commandeService.getListeArticleCommande()) { sb.append("<tr>"); sb.append(createTd(articleCommande.getArticleCatalogue().getReference(), false)); sb.append(createTd(articleCommande.getArticleCatalogue().getLibelle(), false)); sb.append(createTd(articleCommande.getQuantiteRecu().toString(), true)); sb.append(createTd(articleCommande.getQuantiteCommande().toString(), true)); sb.append(createTd(articleCommande.getArticleCatalogue().getLibelleColisage(), false)); sb.append(createTd(articleCommande.getQuantiteReelRecu().toString(), true)); sb.append(createTd(articleCommande.getQuantiteReelCommande().toString(), true)); sb.append("<tr>"); } sb.append("</table>"); return sb.toString(); } private String construiTableauHtmlArticleSpecifique(ArticleSpecifique articleSpecifique) { StringBuilder sb = new StringBuilder(""); sb.append("<table style=\"border-collapse: collapse;\">"); sb.append("<tr bgcolor=\"#7FBFFF\">"); sb.append(createTh("Sous-famille")); sb.append(createTh("Rfrence")); sb.append(createTh("Libell")); sb.append(createTh("Fournisseur")); sb.append(createTh("Colisage")); sb.append(createTh("Quantit demande")); sb.append(createTh("Quantit relle")); sb.append("</tr>"); sb.append(createTdArticleSpecifique(articleSpecifique)); sb.append("</table>"); return sb.toString(); } private String createTdArticleSpecifique(ArticleSpecifique articleSpecifique) { StringBuilder sb = new StringBuilder(""); sb.append("<tr>"); sb.append(createTd( articleSpecifique.getSousFamille() != null ? articleSpecifique.getSousFamille().getLibelle() : "", true)); sb.append(createTd(articleSpecifique.getReference(), true)); sb.append(createTd(articleSpecifique.getLibelle(), true)); sb.append(createTd( articleSpecifique.getFournisseur() != null ? articleSpecifique.getFournisseur().getNom() : "", true)); sb.append(createTd(articleSpecifique.getLibelleColisage(), true)); sb.append(createTd(articleSpecifique.getQuantite().toString(), true)); sb.append(createTd(articleSpecifique.getQuantiteReel().toString(), true)); sb.append("<tr>"); return sb.toString(); } private String construiTableauHtmlRapportStock(List<Object[]> listeNouveauArticleStockAvecQuantite, List<Object[]> listeExistantArticleStockAvecQuantite) { StringBuilder sb = new StringBuilder(""); sb.append("<table style=\"border-collapse: collapse;\">"); sb.append("<tr bgcolor=\"#7FBFFF\">"); sb.append(createTh("Rfrence")); sb.append(createTh("Libell")); sb.append(createTh("Quantit ajoute au stock")); sb.append(createTh("Nouvelle quantit dans le stock")); sb.append(createTh("Observation")); sb.append("</tr>"); Catalogue catalogueActif = catalogueService.findActif(); for (Object[] articleEtQuantite : listeNouveauArticleStockAvecQuantite) { sb.append(createTdGenericStock(catalogueActif, articleEtQuantite, "Nouvel article en stock")); } for (Object[] articleEtQuantite : listeExistantArticleStockAvecQuantite) { sb.append(createTdGenericStock(catalogueActif, articleEtQuantite, "Quantit du stock mise jour")); } sb.append("</table>"); return sb.toString(); } private String createTdGenericStock(Catalogue catalogue, Object[] articleEtQuantite, String observation) { ArticleStock articleStock = ((ArticleStock) articleEtQuantite[0]); ArticleCatalogue articleCatalogue = articleCatalogueService .findArticleCataloguePlusRecentParReference(articleStock.getReferenceArticleStock(), catalogue); StringBuilder sb = new StringBuilder(""); sb.append("<tr>"); sb.append(createTd(articleStock.getReferenceArticleStock(), false)); sb.append(createTd(articleCatalogue.getLibelle(), false)); sb.append(createTd(articleEtQuantite[1].toString(), true)); sb.append(createTd(articleStock.getQuantiteStock().toString(), true)); sb.append(createTd(observation, true)); sb.append("<tr>"); return sb.toString(); } @Override public void sendMailDemandeSpecifique(ArticleSpecifique articleSpecifique) throws UnsupportedEncodingException, MessagingException { AppUser currentUser = authHelper.getCurrentUser(); AppockMail appockMail = new AppockMail(); appockMail.addDestinataire(appUserService.findAllWithRole(Role.ROLE_REFERENT_ACHAT, true)); // On ajoute en destinaitaire la personne connecte et le titulaire du service (si ce n'est pas lui mme) if (!currentUser.isTitulaire()) { // Si ce n'est pas le titulaire, on envoi aussi au titulaire AppUser appUserTitulaire = appUserService.findTitulaireByService(currentUser.getService()); if (appUserTitulaire != null) { appockMail.addDestinataire(appUserTitulaire); } } appockMail.addDestinataire(currentUser); appockMail.setSujet("[APPOCK] Demande d'article spcifique"); String contenu = "Bonjour,<br/><br/>" + currentUser.getNomComplet() + " du service " + currentUser.getService().getLibellePoleDirectionService() + " vous fait parvenir la demande d'article spcifique suivante :<br/><br/>" + construiTableauHtmlArticleSpecifique(articleSpecifique); if (StringUtils.isNotBlank(articleSpecifique.getObservation())) { contenu += "<br/>Motivation jointe la demande :<br/>\"<i>" + articleSpecifique.getObservation() + "\"</i>"; } appockMail.setContenu(contenu); sendMail(appockMail); } @Override public void sendMailDemandeInventaire(Service service) throws UnsupportedEncodingException, MessagingException { AppockMail appockMail = new AppockMail(); appockMail.addDestinataire(appUserRepository.findAllByService(service)); appockMail.setSujet("[APPOCK] Demande d'inventaire pour mise jour de votre stock"); String contenu = "Bonjour,<br/><br/>La section achat vous invite faire un inventaire de votre stock.<br/>Rendez-vous sur l'application APPOCK (menu 'Mon stock') pour procder des ajustements de votre stock, avec la contribution de la section achat si ncessaire. Cliquez ensuite sur le bouton 'Valider l'inventaire'."; appockMail.setContenu(contenu); sendMail(appockMail); } @Override public void sendMailValidationInventaire(Service service) throws UnsupportedEncodingException, MessagingException { AppUser currentUser = authHelper.getCurrentUser(); AppockMail appockMail = new AppockMail(); appockMail.addDestinataire(appUserService.findAllWithRole(Role.ROLE_REFERENT_ACHAT, true)); appockMail.setSujet( "[APPOCK] Validation de l'inventaire du service " + service.getLibellePoleDirectionService()); String contenu = "Bonjour,<br/><br/>" + currentUser.getNomComplet() + " du service " + service.getLibellePoleDirectionService() + " vient de valider son inventaire. Voici ci-dessous l'tat actuel de son stock :<br/><br/>" + construiTableauHtmlStockService(service); appockMail.setContenu(contenu); sendMail(appockMail); } @Override public void sendMailEcheanceCommandeParReferentAchat(LocalDate datePrevisionnelleCommande) throws MessagingException, UnsupportedEncodingException { AppockMail appockMail = new AppockMail(); appockMail.addDestinataire(appUserService.findAllWithRole(Role.ROLE_REFERENT_SERVICE, true)); appockMail.setSujet("[APPOCK] Rappel d'chance de commande"); String contenu = "Bonjour,<br/><br/>Une commande est prvue le <b>" + (datePrevisionnelleCommande == null ? "?" : DateUtil.formatDate(datePrevisionnelleCommande.atStartOfDay())) + "</b> par les rfrents achat. Veuillez envoyer vos demandes d'approvisionnement au plus tard cette date."; appockMail.setContenu(contenu); sendMail(appockMail); } private String construiTableauHtmlStockService(Service service) { StringBuilder sb = new StringBuilder(""); sb.append("<table style=\"border-collapse: collapse;\">"); sb.append("<tr bgcolor=\"#7FBFFF\">"); sb.append(createTh("Rfrence")); sb.append(createTh("Libell")); sb.append(createTh("En stock")); sb.append("</tr>"); service = serviceService.findOneAndChargeElementStock(service.getId()); for (ArticleStock articleStock : service.getStock().getListeArticleStock()) { sb.append("<tr>"); sb.append(createTd(articleStock.getReferenceArticleStock(), false)); sb.append(createTd(articleStock.getArticleCatalogue().getLibelle(), false)); sb.append(createTd(articleStock.getQuantiteStock().toString(), true)); sb.append("</tr>"); } sb.append("</table>"); return sb.toString(); } }