feat(jsqlparser): 首次提交
This commit is contained in:
610
build.gradle
Normal file
610
build.gradle
Normal file
@@ -0,0 +1,610 @@
|
||||
import se.bjurr.gitchangelog.plugin.gradle.GitChangelogTask
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'maven-publish'
|
||||
id 'signing'
|
||||
|
||||
id "org.javacc.javacc" version "latest.release"
|
||||
id 'jacoco'
|
||||
id 'com.github.kt3k.coveralls' version "latest.release"
|
||||
id "com.github.spotbugs" version "latest.release"
|
||||
id "com.diffplug.spotless" version "latest.release"
|
||||
id 'pmd'
|
||||
id 'checkstyle'
|
||||
|
||||
// download the RR tools which have no Maven Repository
|
||||
id "de.undercouch.download" version "latest.release"
|
||||
id 'org.hidetake.ssh' version "latest.release"
|
||||
|
||||
id "se.bjurr.gitchangelog.git-changelog-gradle-plugin" version "latest.release"
|
||||
id "me.champeau.jmh" version "latest.release"
|
||||
id "com.nwalsh.gradle.saxon.saxon-gradle" version "0.9.6"
|
||||
}
|
||||
|
||||
def getVersion = { boolean considerSnapshot ->
|
||||
Integer major = 0
|
||||
Integer minor = 0
|
||||
Integer patch = null
|
||||
Integer build = null
|
||||
def commit = null
|
||||
def snapshot = ""
|
||||
new ByteArrayOutputStream().withStream { os ->
|
||||
exec {
|
||||
args = [
|
||||
"--no-pager"
|
||||
, "describe"
|
||||
, "--tags"
|
||||
, "--always"
|
||||
, "--dirty=-SNAPSHOT"
|
||||
]
|
||||
executable "git"
|
||||
standardOutput = os
|
||||
}
|
||||
def versionStr = os.toString().trim()
|
||||
def pattern = /(?<major>\d*)\.(?<minor>\d*)(\.(?<patch>\d*))?(-(?<build>\d*)-(?<commit>[a-zA-Z\d]*))?/
|
||||
def matcher = versionStr =~ pattern
|
||||
if (matcher.find()) {
|
||||
major = matcher.group('major') as Integer
|
||||
minor = matcher.group('minor') as Integer
|
||||
patch = matcher.group('patch') as Integer
|
||||
build = matcher.group('build') as Integer
|
||||
commit = matcher.group('commit')
|
||||
}
|
||||
|
||||
if (considerSnapshot && ( versionStr.endsWith('SNAPSHOT') || build!=null) ) {
|
||||
minor++
|
||||
if (patch!=null) patch = 0
|
||||
snapshot = "-SNAPSHOT"
|
||||
}
|
||||
}
|
||||
return patch!=null
|
||||
? "${major}.${minor}.${patch}${snapshot}"
|
||||
: "${major}.${minor}${snapshot}"
|
||||
}
|
||||
|
||||
// for publishing a release, call Gradle with Environment Variable RELEASE:
|
||||
// RELEASE=true gradle JSQLParser:publish
|
||||
version = getVersion( !System.getenv("RELEASE") )
|
||||
group = 'com.github.jsqlparser'
|
||||
description = 'JSQLParser library'
|
||||
archivesBaseName = "JSQLParser"
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
|
||||
// Sonatype OSSRH
|
||||
maven {
|
||||
url = uri('https://s01.oss.sonatype.org/content/repositories/snapshots/')
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
xmlDoclet
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'commons-io:commons-io:2.+'
|
||||
testImplementation 'org.apache.commons:commons-text:+'
|
||||
testImplementation 'org.mockito:mockito-core:+'
|
||||
testImplementation 'org.assertj:assertj-core:+'
|
||||
testImplementation 'org.hamcrest:hamcrest-core:+'
|
||||
testImplementation 'org.apache.commons:commons-lang3:+'
|
||||
testImplementation 'com.h2database:h2:+'
|
||||
|
||||
// for JaCoCo Reports
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:+'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:+'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:+'
|
||||
|
||||
// https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter
|
||||
testImplementation 'org.mockito:mockito-junit-jupiter:+'
|
||||
|
||||
// Performance Benchmark
|
||||
testImplementation 'org.openjdk.jmh:jmh-core:+'
|
||||
testImplementation 'org.openjdk.jmh:jmh-generator-annprocess:+'
|
||||
|
||||
// Java Doc in XML Format
|
||||
xmlDoclet 'com.manticore-projects.tools:xml-doclet:+'
|
||||
|
||||
// enforce latest version of JavaCC
|
||||
testImplementation 'net.java.dev.javacc:javacc:+'
|
||||
javacc 'net.java.dev.javacc:javacc:+'
|
||||
}
|
||||
|
||||
compileJavacc {
|
||||
arguments = [grammar_encoding: 'UTF-8', static: 'false', java_template_type: 'modern']
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
|
||||
sourceCompatibility = '11'
|
||||
targetCompatibility = '11'
|
||||
// needed for XML-Doclet to work (since Doclet changed again with Java 13)
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(11))
|
||||
}
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.release = 11
|
||||
}
|
||||
|
||||
javadoc {
|
||||
include("build/generated/javacc/net/sf/jsqlparser/parser/*.java" )
|
||||
if(JavaVersion.current().isJava9Compatible()) {
|
||||
options.addBooleanOption('html5', true)
|
||||
}
|
||||
options.addBooleanOption("Xdoclint:none", true)
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes (
|
||||
"Automatic-Module-Name": "net.sf.jsqlparser"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('xmldoc', Javadoc) {
|
||||
def outFile = reporting.file(
|
||||
version.endsWith("-SNAPSHOT")
|
||||
? "xmlDoclet/javadoc_snapshot.xml"
|
||||
: "xmlDoclet/javadoc_stable.xml"
|
||||
)
|
||||
|
||||
def rstFile = reporting.file(
|
||||
version.endsWith("-SNAPSHOT")
|
||||
? "xmlDoclet/javadoc_snapshot.rst"
|
||||
: "xmlDoclet/javadoc_stable.rst"
|
||||
)
|
||||
|
||||
source = sourceSets.main.allJava
|
||||
// beware: Gradle deletes this folder automatically and there is no switch-off
|
||||
destinationDir = reporting.file("xmlDoclet")
|
||||
options.docletpath = configurations.xmlDoclet.files as List
|
||||
options.doclet = "com.github.markusbernhardt.xmldoclet.XmlDoclet"
|
||||
title = "API $version"
|
||||
options.addBooleanOption("rst", true)
|
||||
options.addBooleanOption("withFloatingToc", Boolean.parseBoolean(System.getenv().getOrDefault("FLOATING_TOC", "true")))
|
||||
options.addStringOption("basePackage", "net.sf.jsqlparser")
|
||||
options.addStringOption("filename", outFile.getName())
|
||||
|
||||
dependsOn(compileJava)
|
||||
doLast {
|
||||
copy {
|
||||
from rstFile
|
||||
into "${projectDir}/src/site/sphinx/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
environment = [ 'EXPORT_TEST_TO_FILE': 'True' ]
|
||||
useJUnitPlatform()
|
||||
|
||||
// set heap size for the test JVM(s)
|
||||
minHeapSize = "128m"
|
||||
maxHeapSize = "1G"
|
||||
}
|
||||
|
||||
coveralls {
|
||||
jacocoReportPath 'build/reports/jacoco/test/jacocoTestReport.xml'
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
// Jacoco can't handle the TokenManager class
|
||||
afterEvaluate {
|
||||
classDirectories.setFrom(files(classDirectories.files.collect {
|
||||
fileTree(dir: it, exclude: [
|
||||
"**CCJSqlParserTokenManager**"
|
||||
])
|
||||
}))
|
||||
}
|
||||
dependsOn test // tests are required to run before generating the report
|
||||
reports {
|
||||
xml.required = false
|
||||
csv.required = false
|
||||
html.outputLocation = layout.buildDirectory.dir('reports/jacoco')
|
||||
}
|
||||
}
|
||||
jacocoTestCoverageVerification {
|
||||
// Jacoco can't handle the TokenManager class
|
||||
afterEvaluate {
|
||||
classDirectories.setFrom(files(classDirectories.files.collect {
|
||||
fileTree(dir: it, exclude: [
|
||||
"**CCJSqlParserTokenManager**"
|
||||
])
|
||||
}))
|
||||
}
|
||||
violationRules {
|
||||
rule {
|
||||
//element = 'CLASS'
|
||||
limit {
|
||||
//@todo: temporarily reduced it 80%, we need to bring that back to 84% accepting the Keywords PR
|
||||
minimum = 0.50
|
||||
}
|
||||
excludes = [
|
||||
'net.sf.jsqlparser.util.validation.*',
|
||||
'net.sf.jsqlparser.**.*Adapter',
|
||||
'net.sf.jsqlparser.parser.**'
|
||||
]
|
||||
}
|
||||
rule {
|
||||
//element = 'CLASS'
|
||||
limit {
|
||||
counter = 'LINE'
|
||||
value = 'MISSEDCOUNT'
|
||||
|
||||
//@todo: temporarily increased to 7000, we need to bring that down to 5500 after accepting the Keywords PR
|
||||
maximum = 20000
|
||||
}
|
||||
excludes = [
|
||||
'net.sf.jsqlparser.util.validation.*',
|
||||
'net.sf.jsqlparser.**.*Adapter',
|
||||
'net.sf.jsqlparser.parser.**'
|
||||
]
|
||||
}
|
||||
// rule {
|
||||
// element = 'CLASS'
|
||||
// limit {
|
||||
// counter = 'LINE'
|
||||
// value = 'MISSEDRATIO'
|
||||
// maximum = 0.3
|
||||
// }
|
||||
// excludes = [
|
||||
// 'net.sf.jsqlparser.util.validation.*',
|
||||
// 'net.sf.jsqlparser.**.*Adapter',
|
||||
// 'net.sf.jsqlparser.parser.**'
|
||||
// ]
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
spotbugsMain {
|
||||
reports {
|
||||
html {
|
||||
enabled = true
|
||||
destination = file("build/reports/spotbugs/main/spotbugs.html")
|
||||
stylesheet = 'fancy-hist.xsl'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spotbugs {
|
||||
// fail only on P1 and without the net.sf.jsqlparser.parser.*
|
||||
excludeFilter = file("config/spotbugs/spotBugsExcludeFilter.xml")
|
||||
|
||||
// do not run over the test, although we should do that eventually
|
||||
spotbugsTest.enabled = false
|
||||
}
|
||||
|
||||
pmd {
|
||||
consoleOutput = true
|
||||
sourceSets = [sourceSets.main]
|
||||
|
||||
// clear the ruleset in order to use configured rules only
|
||||
ruleSets = []
|
||||
|
||||
//rulesMinimumPriority = 1
|
||||
ruleSetFiles = files("config/pmd/ruleset.xml")
|
||||
|
||||
pmdMain {
|
||||
excludes = [
|
||||
"build/generated/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
sourceSets = [sourceSets.main, sourceSets.test]
|
||||
configFile = rootProject.file('config/checkstyle/checkstyle.xml')
|
||||
}
|
||||
|
||||
spotless {
|
||||
// optional: limit format enforcement to just the files changed by this feature branch
|
||||
ratchetFrom 'origin/master'
|
||||
|
||||
format 'misc', {
|
||||
// define the files to apply `misc` to
|
||||
target '*.rst', '*.md', '.gitignore'
|
||||
|
||||
// define the steps to apply to those files
|
||||
trimTrailingWhitespace()
|
||||
indentWithSpaces(4) // or spaces. Takes an integer argument if you don't like 4
|
||||
endWithNewline()
|
||||
}
|
||||
java {
|
||||
indentWithSpaces(4)
|
||||
eclipse().configFile('config/formatter/eclipse-java-google-style.xml')
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Checkstyle).configureEach {
|
||||
reports {
|
||||
xml.required = false
|
||||
html.required = true
|
||||
}
|
||||
excludes = [ "**/module-info.java" ]
|
||||
}
|
||||
|
||||
tasks.register('renderRR') {
|
||||
dependsOn(compileJavacc)
|
||||
doLast {
|
||||
// these WAR files have been provided as a courtesy by Gunther Rademacher
|
||||
// and belong to the RR - Railroad Diagram Generator Project
|
||||
// https://github.com/GuntherRademacher/rr
|
||||
//
|
||||
// Hosting at manticore-projects.com is temporary until a better solution is found
|
||||
// Please do not use these files without Gunther's permission
|
||||
download.run {
|
||||
src 'http://manticore-projects.com/download/convert.war'
|
||||
dest "$buildDir/rr/convert.war"
|
||||
overwrite false
|
||||
onlyIfModified true
|
||||
}
|
||||
|
||||
download.run {
|
||||
src 'http://manticore-projects.com/download/rr.war'
|
||||
dest "$buildDir/rr/rr.war"
|
||||
overwrite false
|
||||
onlyIfModified true
|
||||
tempAndMove true
|
||||
}
|
||||
|
||||
javaexec {
|
||||
standardOutput = new FileOutputStream("${buildDir}/rr/JSqlParserCC.ebnf")
|
||||
main = "-jar"
|
||||
args = [
|
||||
"$buildDir/rr/convert.war",
|
||||
"$buildDir/generated/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jj"
|
||||
]
|
||||
}
|
||||
|
||||
javaexec {
|
||||
main = "-jar"
|
||||
args = [
|
||||
"$buildDir/rr/rr.war",
|
||||
"-noepsilon",
|
||||
"-color:#4D88FF",
|
||||
"-offset:0",
|
||||
"-width:800",
|
||||
//"-png",
|
||||
//"-out:${buildDir}/rr/JSqlParserCC.zip",
|
||||
"-out:${buildDir}/rr/JSqlParserCC.xhtml",
|
||||
"${buildDir}/rr/JSqlParserCC.ebnf"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('gitChangelogTask', GitChangelogTask) {
|
||||
fromRepo = file("$projectDir")
|
||||
file = new File("${projectDir}/src/site/sphinx/changelog.rst")
|
||||
fromRef = "4.0"
|
||||
//toRef = "1.1";
|
||||
|
||||
// switch off the formatter since the indentation matters for Mark-down
|
||||
// @formatter:off
|
||||
templateContent ="""
|
||||
************************
|
||||
Changelog
|
||||
************************
|
||||
|
||||
|
||||
{{#tags}}
|
||||
{{#ifMatches name "^Unreleased.*"}}
|
||||
Latest Changes since |JSQLPARSER_VERSION|
|
||||
{{/ifMatches}}
|
||||
{{#ifMatches name "^(?!Unreleased).*"}}
|
||||
Version {{name}}
|
||||
{{/ifMatches}}
|
||||
=============================================================
|
||||
|
||||
{{#issues}}
|
||||
|
||||
{{#commits}}
|
||||
{{#ifMatches messageTitle "^(?!Merge).*"}}
|
||||
* **{{{messageTitle}}}**
|
||||
|
||||
{{authorName}}, {{commitDate}}
|
||||
{{/ifMatches}}
|
||||
{{/commits}}
|
||||
|
||||
{{/issues}}
|
||||
{{/tags}}
|
||||
"""
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
tasks.register('updateKeywords', JavaExec) {
|
||||
group = "Execution"
|
||||
description = "Run the main class with JavaExecTask"
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
args = [
|
||||
file('src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt').absolutePath
|
||||
, file('src/site/sphinx/keywords.rst').absolutePath
|
||||
]
|
||||
main("net.sf.jsqlparser.parser.ParserKeywordsUtils")
|
||||
|
||||
dependsOn(compileJava)
|
||||
}
|
||||
|
||||
xslt {
|
||||
def outFile = version.endsWith("-SNAPSHOT")
|
||||
? file("src/site/sphinx/syntax_snapshot.rst")
|
||||
: file("src/site/sphinx/syntax_stable.rst")
|
||||
|
||||
dependsOn(renderRR)
|
||||
stylesheet 'src/main/resources/rr/xhtml2rst.xsl'
|
||||
|
||||
parameters (
|
||||
"withFloatingToc": System.getenv().getOrDefault("FLOATING_TOC", "true"),
|
||||
"isSnapshot": Boolean.toString(version.endsWith("-SNAPSHOT"))
|
||||
)
|
||||
|
||||
// Transform every .xml file in the "input" directory.
|
||||
input "$buildDir/rr/JSqlParserCC.xhtml"
|
||||
output outFile
|
||||
}
|
||||
|
||||
tasks.register('sphinx', Exec) {
|
||||
dependsOn(gitChangelogTask, renderRR, xslt, xmldoc)
|
||||
|
||||
String PROLOG = """
|
||||
.. |_| unicode:: U+00A0
|
||||
:trim:
|
||||
|
||||
.. |JSQLPARSER_EMAIL| replace:: support@manticore-projects.com
|
||||
.. |JSQLPARSER_VERSION| replace:: ${getVersion(false)}
|
||||
.. |JSQLPARSER_SNAPSHOT_VERSION| replace:: ${getVersion(true)}
|
||||
.. |JSQLPARSER_STABLE_VERSION_LINK| raw:: html
|
||||
|
||||
<a href='http://manticore-projects.com/download/${project.name}-${getVersion(false)}/${project.name}-${getVersion(false)}.jar'>${project.name}-${getVersion(false)}.jar</a>
|
||||
|
||||
.. |JSQLPARSER_SNAPSHOT_VERSION_LINK| raw:: html
|
||||
|
||||
<a href='http://manticore-projects.com/download/${project.name}-${getVersion(false)}/${project.name}-${getVersion(true)}.jar'>${project.name}-${getVersion(true)}.jar</a>
|
||||
|
||||
"""
|
||||
|
||||
args = [
|
||||
"-Dproject=JSQLParser"
|
||||
, "-Dcopyright=Tobias Warneke, 2022"
|
||||
, "-Dauthor=Tobias Warneke"
|
||||
, "-Drelease=${getVersion(false)}"
|
||||
, "-Drst_prolog=$PROLOG"
|
||||
, "${projectDir}/src/site/sphinx"
|
||||
, "${project.buildDir}/sphinx"
|
||||
]
|
||||
|
||||
executable "sphinx-build"
|
||||
|
||||
//store the output instead of printing to the console:
|
||||
standardOutput = new ByteArrayOutputStream()
|
||||
|
||||
//extension method stopTomcat.output() can be used to obtain the output:
|
||||
ext.output = {
|
||||
return standardOutput.toString()
|
||||
}
|
||||
}
|
||||
|
||||
publish {
|
||||
dependsOn(check, gitChangelogTask, renderRR, xslt, xmldoc)
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
artifactId = 'jsqlparser'
|
||||
|
||||
from components.java
|
||||
versionMapping {
|
||||
usage('java-api') {
|
||||
fromResolutionOf('runtimeClasspath')
|
||||
}
|
||||
usage('java-runtime') {
|
||||
fromResolutionResult()
|
||||
}
|
||||
}
|
||||
pom {
|
||||
name = 'JSQLParser library'
|
||||
description = 'Parse SQL Statements into Abstract Syntax Trees (AST)'
|
||||
url = 'https://github.com/JSQLParser/JSqlParser'
|
||||
licenses {
|
||||
license {
|
||||
name = 'GNU Library or Lesser General Public License (LGPL) V2.1'
|
||||
url = 'http://www.gnu.org/licenses/lgpl-2.1.html'
|
||||
}
|
||||
license {
|
||||
name = 'The Apache Software License, Version 2.0'
|
||||
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id = 'twa'
|
||||
name = 'Tobias Warneke'
|
||||
email = 't.warneke@gmx.net'
|
||||
}
|
||||
developer {
|
||||
id = 'are'
|
||||
name = 'Andreas Reichel'
|
||||
email = 'andreas@manticore-projects.com'
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = 'scm:git:https://github.com/JSQLParser/JSqlParser.git'
|
||||
developerConnection = 'scm:git:ssh://git@github.com:JSQLParser/JSqlParser.git'
|
||||
url = 'https://github.com/JSQLParser/JSqlParser.git'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
maven {
|
||||
name "ossrh"
|
||||
def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
|
||||
def snapshotsRepoUrl= "https://oss.sonatype.org/content/repositories/snapshots/"
|
||||
url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
|
||||
credentials {
|
||||
username = System.getenv("ossrhUsername")
|
||||
password = System.getenv("ossrhPassword")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
//def signingKey = findProperty("signingKey")
|
||||
//def signingPassword = findProperty("signingPassword")
|
||||
//useInMemoryPgpKeys(signingKey, signingPassword)
|
||||
|
||||
// don't sign SNAPSHOTS
|
||||
if (!version.endsWith('SNAPSHOT')) {
|
||||
sign publishing.publications.mavenJava
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
remotes {
|
||||
webServer {
|
||||
host = findProperty("${project.name}.host")
|
||||
user = findProperty("${project.name}.username")
|
||||
identity = new File("${System.properties['user.home']}/.ssh/id_rsa")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('upload') {
|
||||
doFirst {
|
||||
if (findProperty("${project.name}.host") == null) {
|
||||
println(
|
||||
"""
|
||||
Property \"${project.name}.host\' not found.
|
||||
Please define \"${project.name}.host\" in the Gradle configuration (e. g. \$HOME/.gradle/gradle.properties.
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
doLast {
|
||||
ssh.run {
|
||||
session(remotes.webServer) {
|
||||
def versionStable = getVersion(false)
|
||||
execute "mkdir -p download/${project.name}-${versionStable}"
|
||||
for (File file: fileTree(include:['*.jar'], dir:"${project.buildDir}/libs").collect()) {
|
||||
put from: file, into: "download/${project.name}-${versionStable}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
check.dependsOn jacocoTestCoverageVerification
|
||||
upload.dependsOn(check, assemble, gitChangelogTask, renderRR, xslt, xmldoc)
|
||||
|
||||
Reference in New Issue
Block a user