< 2021SC@SDUSC Blog software engineering application and practice of Shandong University JPress code analysis

Keywords: Java

2021SC@SDUSC

preface

In the previous three articles, I disassembled and analyzed the basic frameworks JFinal and JBoot of JPress project at the use level. In the following article, I will analyze the module article in the JPress project at the code level.

JPress is an open source and fast site building product suitable for non large enterprises to reduce development costs. Module article is not only the part of article management in JPress project, but also one of the core parts.

Main Maven project

Maven is a project management tool that can build and manage Java projects. Each Maven project has a basic work unit, called POM (Project Object Model), which is an XML file containing the basic information of the project, which is used to describe how to build the project, declare project dependencies, etc. When executing a task or target, Maven will find and read the POM in the current directory, obtain the required configuration information, and then execute the target.

Module article the POM of Maven project indicates the subprojects of the project and lists the relative path of each subproject element to the module directory.

<modules>
    <module>module-article-model</module>
    <module>module-article-web</module>
    <module>module-article-service</module>
    <module>module-article-service-provider</module>
    <module>module-article-search</module>
    <module>module-article-search-db</module>
    <module>module-article-search-lucene</module>
    <module>module-article-search-es</module>
    <module>module-article-search-opensearch</module>
</modules>

It can be seen that there are 9 modules under module article, including 4 main modules, namely model, web, service and search, and the other 5 modules are the implementation parts of these 4 main modules.

As mentioned earlier, JFinal is designed according to MVC architecture, while JPress project is developed based on JFinal framework, so module article follows the pace of infrastructure architecture design. From this, we can conclude that the module that can guide the whole project process and ultimately lead to key code analysis should be the service of the four main modules, so we will focus on the analysis of the service part.

Before understanding the service, we should first understand the Article Model class.

Article Model class

The module article service provides interfaces and abstract classes for operating articles, article classifications and comments, namely ArticleCategoryService, ArticleCommentService and ArticleService. I will analyze the disassembly step by step. First, I will analyze the part of the article.

Basic structure

To analyze the Article, you first need to understand the structure of the Model class Article of the Article.

@Table(tableName = "article", primaryKey = "id")
public class Article extends BaseArticle<Article> {}

The annotation @ Table is defined in the JBoot framework. The purpose is to automatically generate a Table in the source database at runtime, and take the id as the primary key of the Table.

Inheriting the basearticle < m > abstract class is to automatically generate JavaBean s using the code generator and have a series of operation methods on the Model class.

static const

public static final String STATUS_NORMAL = "normal";
public static final String STATUS_DRAFT = "draft";
public static final String STATUS_TRASH = "trash";

The article has three states: normal, draft and discard. It is named with static and final for subsequent use and modification.

JavaBean

@JsonIgnore
public boolean isNormal() {
    return STATUS_NORMAL.equals(getStatus());
}

@JsonIgnore
public boolean isDraft() {
    return STATUS_DRAFT.equals(getStatus());
}

@JsonIgnore
public boolean isTrash() {
    return STATUS_TRASH.equals(getStatus());
}

The above code has a section of details. When comparing two strings, use "constant equals variable", i.e. "trash".equals(getStatus()), instead of "variable equals constant", i.e. "getStatus().equals("trash "). The fundamental reason is that when the latter is used, when getStatus() cannot query the result and the result is null, the system will cause an error and crash the system. While the former is not null because the constant is not null. Even if getStatus() cannot query the result, the result returned to the caller is still false.

Because the JBoot framework can receive Json by writing a method with the annotation @ JsonBody in the Controller, an annotation is needed to indicate which fields in the Model need to be ignored in the Json transformation, and the annotation @ jsonignor came into being. A field in the Json transformation can be ignored through the annotation @ JsonIgnore. Because the above method conforms to the format of the Json transformation method in the JBoot framework, the annotation @ JsonIgnore must be added. The above code will ignore the following output:

{
    //Example: Jason
    "isNormal": false,
    "isDraft": true,
    "isTrash": false
 }

The above is only a few JavaBean methods of Article Model class. These methods only return the variables of an object, which is of little significance for analysis.

URL acquisition and processing

@JsonIgnore
public String getUrl() {
    String link = getLinkTo();
    if (StrUtil.isNotBlank(link)) {
        return link;
    }
    return UrlUtils.getUrl("/article/", StrUtil.isNotBlank(getSlug()) ? getSlug() : getId());
}

public String getUrlWithPageNumber(int pageNumber) {
    if (pageNumber <= 1) {
        return getUrl();
    }
    return UrlUtils.getUrl("/article/", StrUtil.isNotBlank(getSlug()) ? getSlug() : getId(), "-", pageNumber);
}

The above code is to obtain the URL of the article. Because the article URL is stored as a string in the database, we can get the URL through a series of string tools.

Here we dig into these string tools. The following is the code tracked by IntelliJ IDEA:

//io.jboot.utils.StrUtil.isNotBlank()
public static boolean isNotBlank(Object str) {
    return str != null && notBlank(str.toString());
}

//io.jboot.utils.StrUtil.notBlank()
public static boolean notBlank(String str) {
    return !isBlank(str);
}

//io.jboot.utils.StrUtil.isBlank()
public static boolean isBlank(String str) {
    if (str == null) {
        return true;
    } else {
        int i = 0;
        for(int len = str.length(); i < len; ++i) {
            if (str.charAt(i) > ' ') {
                return false;
            }
        }
        return true;
    }
}

Obviously, the isNotBlank() method here means that the string is neither null nor empty. The key code we get after tracing the source of this method is isBlank(), that is, in the style of C language, we can judge whether the string is empty by traversing each character of the string.

//io.jpress.commons.utils.UrlUtils.getUrl()
public static String getUrl(Object... paths) {
    return buildUrl(paths);
}

//io.jpress.commons.utils.UrlUtils.buildUrl()
private static String buildUrl(Object... paths) {
    boolean isWebFlatUrlEnable = JPressOptions.isFlatUrlEnable();
    StringBuilder url = new StringBuilder(JFinal.me().getContextPath());
    for (int i = 0; i < paths.length; i++) {
        Object path = paths[i];
        if (path == null){
            continue;
        }
        if (isWebFlatUrlEnable) {
            url.append(toFlat(i, path.toString()));
        } else {
            url.append(path);
        }
    }
    return url.append(JPressOptions.getAppUrlSuffix()).toString();
}

//io.jpress.commons.utils.UrlUtils.toFlat()
private static String toFlat(int index, String path) {
    char[] chars = new char[path.length()];
    path.getChars(index == 0 ? 1 : 0, chars.length, chars, index == 0 ? 1 : 0);
    for (int i = 0; i < chars.length; i++) {
        if (chars[i] == '/') {
            chars[i] = '-';
        }
    }
    if (index == 0) {
        chars[0] = '/';
    }
    return new String(chars);
}

The buildUrl() method first determines whether isWebFlatUrlEnable is enabled in the configuration file. This configuration means whether the URL is flat under the JFinal framework. If it is enabled, toFlat() will be performed. In fact, it is not easy to translate here, and the toflat () method converts the URL of / xxx/xxx type into / xxx xxx xxx, so the developer regards the URL of / xxx xxx type as the URL of the platform, and / xxx/xxx as uneven.

Finally, the buildUrl() method returns the created URL to the Service layer.

Interaction with database

//io.jboot.db.model.JbootModel.save()
@Override
public boolean save() {
    CommonsUtils.escapeModel(this, "content", "summary");
    JsoupUtils.clean(this,"content","summary");
    return super.save();
}

//io.jboot.db.model.JbootModel.updates()
@Override
public boolean update() {
    CommonsUtils.escapeModel(this, "content", "summary");
    JsoupUtils.clean(this,"content","summary");
    return super.update();
}

//io.jpress.commons.utils.CommonsUtils.escapeModel()
public static void escapeModel(Model model, String... ignoreAttrs) {
    String[] attrNames = model._getAttrNames();
    for (String attr : attrNames) {

        if (ArrayUtils.contains(ignoreAttrs, attr)) {
            continue;
        }

        Object value = model.get(attr);

        if (value != null && value instanceof String) {
            model.set(attr, StrUtil.escapeHtml(value.toString()));
        }
    }
}

//io.jpress.commons.utils.JsoupUtils.clean()
public static void clean(Model model, String... attrs) {
    if (attrs != null && attrs.length == 0) {
        return;
    }

    for (String attr : attrs) {
        Object data = model.get(attr);
        if (data == null || !(data instanceof String)) {
            continue;
        }

        model.set(attr, clean((String) data));
    }
}

This is the method to store the JBoot framework Model into the database. The escapeModel() and clean() methods are declared by the JPress project. These two methods clear the content and summary fields to prevent the Model from storing code related to XSS cross site scripting attacks.

The following code is the save() and update() methods used in JFinal to interface with the database:

//com.jfinal.plugin.activerecord.Model.save()
public boolean save() {
    this.filter(0);
    Config config = this._getConfig();
    Table table = this._getTable();
    StringBuilder sql = new StringBuilder();
    List<Object> paras = new ArrayList();
    config.dialect.forModelSave(table, this.attrs, sql, paras);
    Connection conn = null;
    PreparedStatement pst = null;
    boolean var7 = false;
    
    boolean var8;
    try {
        conn = config.getConnection();
        if (config.dialect.isOracle()) {
            pst = conn.prepareStatement(sql.toString(), table.getPrimaryKey());
        } else {
            pst = conn.prepareStatement(sql.toString(), 1);
        }

        config.dialect.fillStatement(pst, paras);
        int result = pst.executeUpdate();
        config.dialect.getModelGeneratedKey(this, pst, table);
        this._getModifyFlag().clear();
        var8 = result >= 1;
    } catch (Exception var12) {
        throw new ActiveRecordException(var12);
    } finally {
        config.close(pst, conn);
    }

    return var8;
}

//com.jfinal.plugin.activerecord.Model.update()
public boolean update() {
    this.filter(1);
    if (this._getModifyFlag().isEmpty()) {
        return false;
    } else {
        Table table = this._getTable();
        String[] pKeys = table.getPrimaryKey();
        String[] var3 = pKeys;
        int var4 = pKeys.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            String pKey = var3[var5];
            Object id = this.attrs.get(pKey);
            if (id == null) {
                throw new ActiveRecordException("You can't update model without Primary Key, " + pKey + " can not be null.");
            }
        }

        Config config = this._getConfig();
        StringBuilder sql = new StringBuilder();
        List<Object> paras = new ArrayList();
        config.dialect.forModelUpdate(table, this.attrs, this._getModifyFlag(), sql, paras);
        if (paras.size() <= 1) {
            return false;
        } else {
            Connection conn = null;

            boolean var8;
            try {
                conn = config.getConnection();
                int result = Db.update(config, conn, sql.toString(), paras.toArray());
                if (result < 1) {
                    var8 = false;
                    return var8;
                }

                this._getModifyFlag().clear();
                var8 = true;
            } catch (Exception var12) {
                throw new ActiveRecordException(var12);
            } finally {
                config.close(conn);
            }

            return var8;
        }
    }
}

epilogue

This paper analyzes the Model class Article under the module Article module of JPress project, and analyzes the method of this entity class in detail. In the next Article, we will analyze the module Article service of this module in detail.

Posted by csn on Sun, 31 Oct 2021 12:01:07 -0700