Java tutorial
/** * Axelor Business Solutions * * Copyright (C) 2016 Axelor (<http://axelor.com>). * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.axelor.studio.service.builder; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.commons.lang.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.axelor.common.Inflector; import com.axelor.common.VersionUtils; import com.axelor.exception.AxelorException; import com.axelor.i18n.I18n; import com.axelor.meta.db.MetaField; import com.axelor.meta.db.MetaModel; import com.axelor.meta.db.MetaModule; import com.axelor.meta.db.MetaSequence; import com.axelor.meta.db.MetaTranslation; import com.axelor.meta.db.repo.MetaFieldRepository; import com.axelor.meta.db.repo.MetaModelRepository; import com.axelor.meta.db.repo.MetaTranslationRepository; import com.axelor.studio.service.ConfigurationService; import com.axelor.studio.utils.Namming; import com.google.common.base.Strings; import com.google.inject.Inject; import com.google.inject.persist.Transactional; /** * Service class generate domain xml files from MetaModels. It process MetaModel * to create model and MetaField to create field xml. * * @author axelor * */ public class ModelBuilderService { private final static List<String> reserveFields; static { List<String> fields = new ArrayList<String>(); fields.add("id"); fields.add("version"); fields.add("createdBy"); fields.add("updatedBy"); fields.add("createdOn"); fields.add("updatedOn"); fields.add("archived"); reserveFields = Collections.unmodifiableList(fields); } private static final String NAMESPACE = "http://axelor.com/xml/ns/domain-models"; private static final String VERSION = VersionUtils.getVersion().feature; private final Logger log = LoggerFactory.getLogger(getClass()); private Map<String, StringBuilder> moduleSequenceMap; private Map<String, StringBuilder> moduleFieldMap; private List<String> trackFields; @Inject private MetaModelRepository metaModelRepo; @Inject private MetaTranslationRepository translationRepo; @Inject private MetaFieldRepository metaFieldRepo; @Inject private ConfigurationService configService; public static boolean isReserved(String name) { return reserveFields.contains(name); } /** * Root method to access the service. It will find all edited and * customised MetaModels. Call other methods to process MetaModel founds. * @throws AxelorException */ public void build() throws AxelorException { try { List<MetaModel> customizedModels = metaModelRepo.all().filter("self.customised = true").fetch(); checkFiles(customizedModels); for (MetaModel model : customizedModels) { if (model.getEdited()) { recordModel(model); updateEdited(model); } } } catch (IOException e) { e.printStackTrace(); throw new AxelorException(I18n.get("Error in model recording: %s"), 4, e.getMessage()); } } /** * Method to reset edited boolean to false when processing of model * completed. * * @param models * List of MetaModel to process. */ @Transactional public void updateEdited(MetaModel model) { model.setEdited(false); metaModelRepo.save(model); } /** * Method create domain xml file from MetaModel. It create one single domain * xml string for domain and write it to file. Also call method to process * fields. * * @param modelIterator * MetaModel iterator * @throws IOException * Exception thrown by file handling of domain xml file. * @throws AxelorException */ private void recordModel(MetaModel metaModel) throws IOException, AxelorException { moduleSequenceMap = new HashMap<String, StringBuilder>(); moduleFieldMap = new HashMap<String, StringBuilder>(); String packageName = metaModel.getPackageName(); String modelName = metaModel.getName(); trackFields = new ArrayList<String>(); updateSequenceXml(metaModel.getMetaSequenceList().iterator()); List<MetaField> customFields = getCustomisedFields(metaModel, true); if (customFields.isEmpty()) { log.debug("Removing a model with no custom field : {}", metaModel.getName()); configService.removeDomainFile(metaModel.getName() + ".xml"); return; } sortFieldList(customFields); addFields(customFields.iterator()); for (String module : moduleFieldMap.keySet()) { String sequenceXml = ""; StringBuilder sequenceBuilder = moduleSequenceMap.get(module); if (sequenceBuilder == null) { sequenceXml = sequenceXml.toString(); } String fieldXml = moduleFieldMap.get(module).toString(); StringBuilder sb = new StringBuilder("<?xml version='1.0' encoding='UTF-8'?>\n") .append("<domain-models").append(" xmlns='").append(NAMESPACE).append("'") .append(" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'") .append(" xsi:schemaLocation='").append(NAMESPACE).append(" ") .append(NAMESPACE + "/" + "domain-models_" + VERSION + ".xsd").append("'").append(">\n\n") .append("\t<module name=\"" + module.replace("axelor-", "") + "\" package=\"" + packageName + "\" />\n\n") .append(sequenceXml + "\n").append("\t<entity name=\"" + modelName + "\">\n").append(fieldXml) .append(getTrackFields()).append("\t</entity>\n\n").append("</domain-models>\n"); File domainFile = new File(configService.getDomainDir(module, true), modelName + ".xml"); writeFile(domainFile, sb.toString()); } } /** * This method process MetaField and create field xml from it. The generated * field xml will be appended to model xml. * * @param fieldIterator * MetaField iterator * @throws AxelorException */ private void addFields(Iterator<MetaField> fieldIterator) throws AxelorException { if (!fieldIterator.hasNext()) { return; } MetaField field = fieldIterator.next(); MetaModule module = field.getMetaModule(); if (module == null) { module = field.getMetaModel().getMetaModule(); } if (module == null || !module.getCustomised()) { addFields(fieldIterator); return; } String fieldType = field.getFieldType(); StringBuilder fieldXml = moduleFieldMap.get(module.getName()); if (fieldXml == null) { fieldXml = new StringBuilder(); } fieldXml.append("\t\t<" + fieldType + " "); String name = field.getName(); if (reserveFields.contains(name)) { throw new AxelorException( I18n.get("In model '%s' field name '%s' is reserve word. " + "Please use different one"), 5, field.getMetaModel().getName(), name); } if (field.getTrack()) { trackFields.add(name); } fieldXml.append("name=\"" + name + "\" "); if (Namming.isKeyword(name)) { fieldXml.append("column=\"" + Inflector.getInstance().underscore(name) + "_val\" "); } String title = field.getLabel(); if (title != null) { title = title.replace("\\", "\\\\"); title = title.replace("\"", ""); title = StringEscapeUtils.escapeXml(title); fieldXml.append("title=\"" + title + "\" "); } switch (fieldType) { case "integer": addInteger(fieldXml, field); break; case "decimal": addDecimal(fieldXml, field); break; case "string": addString(fieldXml, field); break; case "boolean": addBoolean(fieldXml, field); break; case "many-to-one": addRelational(fieldXml, field); break; case "one-to-many": addRelational(fieldXml, field); break; case "many-to-many": addRelational(fieldXml, field); break; case "one-to-one": addRelational(fieldXml, field); break; } if (field.getMetaSelect() != null) { fieldXml.append("selection=\"" + field.getMetaSelect().getName() + "\" "); } if (field.getRequired()) { fieldXml.append("required=\"true\" "); } if (field.getReadonly()) { fieldXml.append("readonly=\"true\" "); } if (field.getHidden()) { fieldXml.append("hidden=\"true\" "); } if (field.getHelpText() != null) { String help = field.getHelpText(); help = StringEscapeUtils.escapeJava(help); help = StringEscapeUtils.escapeXml(help); fieldXml.append("help=\"" + help + "\" "); } if (field.getNameColumn()) { fieldXml.append("namecolumn=\"true\" "); } fieldXml.append("/>\n"); moduleFieldMap.put(module.getName(), fieldXml); addFields(fieldIterator); } /** * Special method to append min/max attributes to field xml for string and * integer type of fields. * * @param field * MetaField to process */ private void addMinMax(StringBuilder fieldXml, MetaField field) { Integer min = field.getIntegerMin(); if (min != null && min != 0) { fieldXml.append("min=\"" + min + "\" "); } Integer max = field.getIntegerMax(); if (max != null && max != 0) { fieldXml.append("max=\"" + max + "\" "); } } /** * Method to set default value and min/max for integer type of field. * * @param field * MetaField to process */ private void addInteger(StringBuilder fieldXml, MetaField field) { Integer defaultInt = field.getDefaultInteger(); if (defaultInt != null && defaultInt != 0) { fieldXml.append("default=\"" + defaultInt + "\" "); } addMinMax(fieldXml, field); } /** * Method to process decimal type of field for default,min,max attributes * and append it to field xml. * * @param field * MetaField to process */ private void addDecimal(StringBuilder fieldXml, MetaField field) { BigDecimal max = field.getDecimalMax(); if (max != null && max.compareTo(BigDecimal.ZERO) != 0) { fieldXml.append("max=\"" + max + "\" "); } BigDecimal min = field.getDecimalMin(); if (min != null && min.compareTo(BigDecimal.ZERO) != 0) { fieldXml.append("min=\"" + min + "\" "); } BigDecimal defaultDecimal = field.getDefaultDecimal(); if (defaultDecimal != null && defaultDecimal.compareTo(BigDecimal.ZERO) != 0) { fieldXml.append("default=\"" + defaultDecimal + "\" "); } } /** * Method to process string type of field for default,large,min,max * attributes and append it to field xml. * * @param field * MetaField to process */ private void addString(StringBuilder fieldXml, MetaField field) { Boolean large = field.getLarge(); if (large != null && large) { fieldXml.append("large=\"true\" "); } String defaultString = field.getDefaultString(); if (defaultString != null) { fieldXml.append("default=\"" + defaultString + "\" "); } String sequence = field.getMetaSequence(); if (sequence != null) { fieldXml.append("sequence=\"" + sequence + "\" "); } addMinMax(fieldXml, field); } /** * Method to process boolean field for default attributes and append it to * field xml. * * @param field * MetaField to process */ private void addBoolean(StringBuilder fieldXml, MetaField field) { Boolean defaultBoolean = field.getDefaultBoolean(); if (defaultBoolean != null && defaultBoolean) { fieldXml.append("default=\"true\" "); } } /** * Method to add 'ref' attribute in field xml for relational field. It * search typeName of MetaField for reference model and set fullName as * reference. * * @param field * MetaField to process */ private void addRelational(StringBuilder fieldXml, MetaField field) { log.debug("Type name for relational field : {}, typeName: {}", field.getName(), field.getTypeName()); MetaModel metaModel = metaModelRepo.findByName(field.getTypeName()); if (metaModel != null) { log.debug("Meta model found: {}", metaModel.getName()); fieldXml.append("ref=\"" + metaModel.getFullName() + "\" "); } String mappedBy = field.getMappedBy(); if (!Strings.isNullOrEmpty(mappedBy)) { fieldXml.append("mappedBy=\"" + mappedBy + "\" "); } } /** * Method to write domain xml file. * * @param file * File to write. * @param content * Content to write in file. * @throws IOException * Exception thrown by file handling. */ private void writeFile(File file, String content) throws IOException { log.debug("Writing file: {}", file.getPath()); if (!file.exists()) { file.createNewFile(); } FileWriter fileWriter = new FileWriter(file); fileWriter.write(content); fileWriter.close(); } /** * Sort list of MetaFields according to sequence. * * @param fieldList */ public void sortFieldList(List<MetaField> fieldList) { Comparator<MetaField> comparator = new Comparator<MetaField>() { @Override public int compare(MetaField field1, MetaField field2) { if (field1.getSequence() < field2.getSequence()) { return -1; } return 0; } }; Collections.sort(fieldList, comparator); } /** * Method filter customised field from list MetaField. Only customised field * will be processed and added in domain xml file. * * @param fields * List of MetaField to filter. * @return List of customised field. */ public List<MetaField> getCustomisedFields(MetaModel metaModel, boolean getWkf) { String query = "self.metaModel = ?1 and self.customised = true"; if (!getWkf) { query = "self.metaModel = ?1 and self.customised = true and self.name != 'wkfStatus'"; } List<MetaField> customFields = metaFieldRepo.all().filter(query, metaModel).order("sequence").fetch(); return customFields; } /** * Method create/update MetaTranslation for help text of a field. * * @param key * Key for translation. * @param value * Value for translation. * @param language * Translation language to update. */ @Transactional public void updateTranslation(String key, String value, String language) { if (key != null && language != null) { MetaTranslation translation = translationRepo.all() .filter("self.key = ? AND self.language = ?", key, language).fetchOne(); if (translation == null) { translation = new MetaTranslation(); translation.setKey(key); translation.setLanguage(language); } translation.setMessage(value); translationRepo.save(translation); } } /** * Method convert simple type name of field into adk supported 'typeName' * (which is type class name) for MetaField. * * @param metaField * MetaField to get typeName. * @return Adk supported typeName. */ public String getFieldTypeName(String type) { switch (type) { case "string": return "String"; case "integer": return "Integer"; case "boolean": return "Boolean"; case "decimal": return "BigDecimal"; case "long": return "Long"; case "binary": return "byte[]"; case "date": return "LocalDate"; case "datetime": return "ZonedDateTime"; default: return null; } } /** * It will check the domain files of all the customised modules. * It removes a file if the related model is not present in customizedModels. * @param customizedModels List of the models used in file name comparison. * @throws AxelorException */ private void checkFiles(List<MetaModel> customizedModels) throws AxelorException { List<String> fileNames = new ArrayList<String>(); for (MetaModel model : customizedModels) { fileNames.add(model.getName() + ".xml"); } for (MetaModule module : configService.getCustomizedModules()) { File moduleDir = configService.getDomainDir(module.getName(), false); if (moduleDir == null) { continue; } for (File file : moduleDir.listFiles()) { if (!fileNames.contains(file.getName()) && !file.getName().equals("Selection.xml")) { log.debug("Removing file: {}", file.getName()); file.delete(); } } } } private String getTrackFields() { String track = ""; if (trackFields.isEmpty()) { return track; } track = "\n\t\t<track>"; for (String field : trackFields) { track += "\n\t\t\t<field name=\"" + field + "\"/>"; } track += "\n\t\t</track>\n"; return track; } @Transactional public void updateSequenceXml(Iterator<MetaSequence> seqIter) { if (!seqIter.hasNext()) { return; } MetaSequence sequence = seqIter.next(); MetaModule module = sequence.getMetaModule(); if (module == null || !module.getCustomised()) { updateSequenceXml(seqIter); return; } String name = sequence.getName(); String seqXml = "\t<sequence name=\"" + name + "\" "; String prefix = sequence.getPrefix(); if (prefix != null) { seqXml += "prefix=\"" + prefix + "\" "; } String suffix = sequence.getSuffix(); if (suffix != null) { seqXml += "suffix=\"" + suffix + "\" "; } Integer padding = sequence.getPadding(); if (padding != null) { seqXml += "padding=\"" + padding + "\" "; } seqXml += "/>\n"; StringBuilder sequenceXml = moduleSequenceMap.get(module.getName()); if (sequenceXml == null) { sequenceXml = new StringBuilder(); } sequenceXml.append(seqXml); moduleSequenceMap.put(module.getName(), sequenceXml); updateSequenceXml(seqIter); } }