@@ -0,0 +1,211 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.model ;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument ;
import java.io.Serializable ;
import java.util.Arrays ;
import java.util.Objects ;
import java.util.regex.Matcher ;
import java.util.regex.Pattern ;
import javax.annotation.Nullable ;
import com.google.common.base.Splitter ;
import xyz.zhouxy.plusone.commons.util.StringTools ;
// TODO [优化] 优化正则表达式
// TODO [补充] 完善单元测试
// TODO [doc] javadoc、README.md
/**
* SemVer 语义版本号
*
* @author ZhouXY108 <luquanlion@outlook.com>
* @since 1.1.0
*
* @see <a href="https://semver.org/">Semantic Versioning 2.0.0</a>
*/
public class SemVer implements Comparable < SemVer > , Serializable {
private static final long serialVersionUID = 458265121025514002L ;
private final String value ;
private final int [ ] versionNumbers ;
private final String [ ] preReleaseVersion ;
private final String buildMetadata ;
private static final String VERSION_NUMBERS = " (?<numbers>(?<major>0|[1-9] \\ d*) \\ .(?<minor>0|[1-9] \\ d*) \\ .(?<patch>0|[1-9] \\ d*)( \\ .(0|[1-9] \\ d*)){0,2}) " ;
private static final String PRE_RELEASE_VERSION = " (?:-(?<prerelease>(?:0|[1-9] \\ d*| \\ d*[a-zA-Z-][0-9a-zA-Z-]*)(?: \\ .(?:0|[1-9] \\ d*| \\ d*[a-zA-Z-][0-9a-zA-Z-]*))*))? " ;
private static final String BUILD_METADATA = " (?: \\ +(?<buildmetadata>[0-9a-zA-Z-]+(?: \\ .[0-9a-zA-Z-]+)*))? " ;
private static final Pattern PATTERN = Pattern . compile (
" ^ " + VERSION_NUMBERS + PRE_RELEASE_VERSION + BUILD_METADATA + " $ " ) ;
private SemVer ( String value , int [ ] versionNumbers , @Nullable String [ ] preReleaseVersion , @Nullable String buildMetadata ) {
this . value = value ;
this . versionNumbers = versionNumbers ;
this . preReleaseVersion = preReleaseVersion ;
this . buildMetadata = buildMetadata ;
}
public static SemVer of ( final String value ) {
checkArgument ( StringTools . isNotBlank ( value ) , " 版本号不能为空 " ) ;
final Matcher matcher = PATTERN . matcher ( value ) ;
checkArgument ( matcher . matches ( ) , " 版本号格式错误 " ) ;
// 数字版本部分
final String versionNumbersPart = matcher . group ( " numbers " ) ;
// 先行版本号部分
final String preReleaseVersionPart = matcher . group ( " prerelease " ) ;
// 版本编译信息部分
final String buildMetadataPart = matcher . group ( " buildmetadata " ) ;
final int [ ] versionNumbers = Splitter . on ( '.' )
. splitToStream ( versionNumbersPart )
// 必须都是数字
. mapToInt ( Integer : : parseInt )
. toArray ( ) ;
final String [ ] preReleaseVersion = preReleaseVersionPart ! = null
? Splitter . on ( '.' ) . splitToStream ( preReleaseVersionPart ) . toArray ( String [ ] : : new )
: null ;
return new SemVer ( value , versionNumbers , preReleaseVersion , buildMetadataPart ) ;
}
public int getMajor ( ) {
return this . versionNumbers [ 0 ] ;
}
public int getMinor ( ) {
return this . versionNumbers [ 1 ] ;
}
public int getPatch ( ) {
return this . versionNumbers [ 2 ] ;
}
public String getBuildMetadata ( ) {
return buildMetadata ;
}
@Override
public int compareTo ( @Nullable SemVer that ) {
if ( that = = null ) {
return 1 ;
}
int result = compareVersionNumbers ( that ) ;
if ( result ! = 0 ) {
return result ;
}
return comparePreReleaseVersion ( that ) ;
}
public String getValue ( ) {
return value ;
}
@Override
public int hashCode ( ) {
final int prime = 31 ;
int result = 1 ;
result = prime * result + Arrays . hashCode ( versionNumbers ) ;
result = prime * result + Arrays . hashCode ( preReleaseVersion ) ;
result = prime * result + Objects . hash ( value , buildMetadata ) ;
return result ;
}
@Override
public boolean equals ( @Nullable Object obj ) {
if ( this = = obj )
return true ;
if ( ! ( obj instanceof SemVer ) )
return false ;
SemVer other = ( SemVer ) obj ;
return Objects . equals ( value , other . value ) & & Arrays . equals ( versionNumbers , other . versionNumbers )
& & Arrays . equals ( preReleaseVersion , other . preReleaseVersion )
& & Objects . equals ( buildMetadata , other . buildMetadata ) ;
}
@Override
public String toString ( ) {
return 'v' + value ;
}
private int compareVersionNumbers ( SemVer that ) {
final int minLength = Integer . min ( this . versionNumbers . length , that . versionNumbers . length ) ;
for ( int i = 0 ; i < minLength ; i + + ) {
final int currentVersionNumberOfThis = this . versionNumbers [ i ] ;
final int currentVersionNumberOfThat = that . versionNumbers [ i ] ;
if ( currentVersionNumberOfThis ! = currentVersionNumberOfThat ) {
return currentVersionNumberOfThis - currentVersionNumberOfThat ;
}
}
return this . versionNumbers . length - that . versionNumbers . length ;
}
private int comparePreReleaseVersion ( SemVer that ) {
final String [ ] preReleaseVersionOfThis = this . preReleaseVersion ;
final String [ ] preReleaseVersionOfThat = that . preReleaseVersion ;
byte thisWithoutPreReleaseVersionFlag = preReleaseVersionOfThis = = null ? ( byte ) 1 : ( byte ) 0 ;
byte thatWithoutPreReleaseVersionFlag = preReleaseVersionOfThat = = null ? ( byte ) 1 : ( byte ) 0 ;
if ( ( thisWithoutPreReleaseVersionFlag | thatWithoutPreReleaseVersionFlag ) = = 1 ) {
return thisWithoutPreReleaseVersionFlag - thatWithoutPreReleaseVersionFlag ;
}
@SuppressWarnings ( " null " )
final int minLength = Integer . min ( preReleaseVersionOfThis . length , preReleaseVersionOfThat . length ) ;
for ( int i = 0 ; i < minLength ; i + + ) {
int r = comparePartOfPreReleaseVersion ( preReleaseVersionOfThis [ i ] , preReleaseVersionOfThat [ i ] ) ;
if ( r ! = 0 ) {
return r ;
}
}
return preReleaseVersionOfThis . length - preReleaseVersionOfThat . length ;
}
private static int comparePartOfPreReleaseVersion ( String p1 , String p2 ) {
boolean p1IsNumber = isAllDigits ( p1 ) ;
boolean p2IsNumber = isAllDigits ( p2 ) ;
if ( p1IsNumber ) {
return p2IsNumber
? Integer . parseInt ( p1 ) - Integer . parseInt ( p2 ) // 都是数字
: - 1 ; // p1 是数字, p2 是字符串
}
// 如果 p1 是字符串, p2 是数字,则返回 1( 字符串优先于纯数字)
return p2IsNumber ? 1 : p1 . compareTo ( p2 ) ;
}
private static boolean isAllDigits ( @Nullable String str ) {
if ( str = = null | | str . isEmpty ( ) ) {
return false ;
}
for ( int i = 0 ; i < str . length ( ) ; i + + ) {
char c = str . charAt ( i ) ;
if ( c < '0' | | c > '9' ) {
return false ;
}
}
return true ;
}
}