sábado, 30 de julio de 2022

Gradle-7 (13) Gradle Draft(1). Basics

 0. Prerrequisites

1. Set the JAVA_HOME to a Java installation. Java 17 does NOT work well with gradle

1.a In the ~/.bashrc file for a single user

export JAVA_HOME=<path to your version of java>
export PATH=${PATH}:${JAVA_HOME}/bin

1.b in the /etc/profile file for all users

JAVA_HOME=<path to your version of java>
PATH=$PATH:$HOME/bin:$JAVA_HOME/bin export JAVA_HOME export PATH

1.c in the /etc/environment file 

JAVA_HOME=<path to your version of java>
export JAVA_HOME

1.d Execute  export JAVA_HOME=<path to your version of java>

2. Download gradle from download page

3. Set the PATH variable to gradle, using any of the above methods

4. Create a folder (e.g. MyProject), with a terminal go into this folder and type gradle init

5. Import the gradle project in Eclipse (File->Import->Gradle-> Existing Gradle project)

1. settings.gradle


rootProjectName=myProject
println 'Initialization Phase'

2. build.gradle

println 'Printing...'
def a = 5 ; def b=6 ; println a +b + ' Hola world ---'
println 'Configuration Phase'

description 'Just my project'
group 'com.ximodante'
version '1.0-SNAPSHOT'


3. gradlew

./gradlew tasks
./gradlew clean
./gradlew copyMessage
./gradlew cM
./gradlew --version
./gradlew wrapper --gradle-version=7.5


4. Tasks

import org.apache.tools.ant.filters.ReplaceTokens

tasks.register('copyMessage', Copy) { //Task with a class (Copy)
    group 'My Own Tasks'    // show tasks grouped when using "gradle tasks"
    description 'Copy and replace template files with substitution text'
    from 'myFile.txt'
    into "$BuildDir/myFiles"
    filter (ReplaceTokens, tokens: [TO_REPLACE: "String replacement"])
}

tasks.register('zipDescriptions',  Zip) {
    group 'My Own Tasks'
    description 'zip files'
    from "$buildDir/myFiles"  //Approach 1

    //Indicates that the output of task copyMessabe is the input (recommended)
    // and does NOT need the dependsOn 
    from copyMessage          //Approach 2       

destinationDirectory = buildDir archiveFileName = 'descriptions.zip'
    //Indicates that this task (copyMessage) must be executed before.
    //DO NOT USE IF input points to the previous task!!!!
    dependsOn task.named('copyMessage')     //Approach 1
    dependsOn copyMessage                   //Approach 2
dependsOn copyMessage, otherTask, ... //Several tasks dependences

    //Indivcates that this tasks shouild be executed after
    finalizedBy anotherTask           

    //To throw an exceptio!!!
    doLast {
        throw new GradleException ('My exception is thrown!!!!')      
    }
}

//Not recommended task definition
task('zipDescriptions2', type: Zip) { //Lower performace
    group 'My Own Tasks'
    description 'zip files'
    from "$buildDir/myFiles"
    destinationDirectory = buildDir
    archiveFileName = 'descriptions.zip'
}

//Adhoc task that is not an instance of another class
tasks.register('adhocTask') { //Task without a class
    doFirst {  //closure
       println 'hello!' 
    }
    doLast {  //closure
       println 'Bye!' 
    }
    enabled false // or true
    onlyif { // similar to enabled
       5 == 3 +2 
    }
}

//Task that uses the same name as the first task but copies to another directory
tasks.named('copyMessage') { 
    into "$BuildDir/myFiles-bis"
}

//Task that uses the same name as the first task but copies to another directory
// NOT recommended!!!
tasks.getByName('copyMessage') { 
    into "$BuildDir/myFiles-bis-bis"
}

//Call a task by its name (usually tasks added by plugin
tasks.clean { 
    doLast {
        println "Squeaky clean"
    }    
}

//Call a task by its name (usually tasks added by plugin but simpler
clean { 
    doLast {
        println "Squeaky clean"
    }    
}


4.1 Tasks documentation

https://docs.gradle.org/current/dsl/index.html, look for tasks and can see several defined tasks (Delete, Copy, jar..),  For task Delete:

tasks.register('myDelete', Delete) { //Task with a class (Copy)
    group 'My Own Tasks'    // show tasks grouped when using "gradle tasks"
    description 'Delete some stuff'
    //followSymlinks = true
}

4.2 Tasks graph

The taskinfo plugin shows us the task graph

plugins {
    id 'base'
    id "org.barfuin.gradle.taskinfo" version "1.3.0"
}

To see the tasks ->  ./gradlew build tiTree



5. Plugins

plugins {
    id 'base'
    id "org.barfuin.gradle.taskinfo" version "1.3.0" //Other plugins
}

//Alternative to "plugins" NOT RCOMMENDED
apply plugin: 'base'


// archiveFileName is defined by default in plugin id to 'gradle'
// archiveFileName is a variable that can be used in any task
// there are also other defined defined by default variables taht can be changed
base {
    archivesName = 'stuff'
}



5.1 Plugins documentation

https://plugins.gradle.org/

6. Gradle 

gradle init
1: Basic, 2: Application, 3: library, 4:Gradle plugin 
1: Groovy, 2: Kotlin

gradle wrapper


7. Git

git status
git init
git add .
git commit -m "First commit of the project"

.gitignore

8. Groovy

JVM language
Can use any standard java library
dynamic typed
less verbose (optional semicolons, run code as script, optional parenthesis)
support closures (block of code passed as a variable)
pass closures outside brackets.

9. Kotlin

JVM language
statically typed
string double quotes
has functions but NO methods
need parenthesis
closures 
pass closures outside brackets
IntelliJ suggestions of code


9.1 Example of build.gradle.kts


import org.apache.tools.ant.filters.ReplaceTokens

plugins {
    base
}

tasks.register<Copy> ("generateDescriptions") {
    from ("descriptions")
    into ("$buildDir/descriptions")
    filters(ReplaceTokens::class, "tokens" to mapOf("TO_REPLACE" to "String replacement"))
}


10. Gradle api & documentation



11. Repositories and dependencies

Maven 
Ivy
Flat directory

Built-in repositories: (Maven Central , Google Maven)

repoitories {
    mavenCentral()
    google()
    maven {
        // Custom repository
        url 'https://tomgregory-2994844798587.d.codeartifact.eu-west-1amazonaws.com/maven/demo/'
    }
}

dependencies {
    implementation ('commons-beanutls:commons-beanutls:1.9.4) {
        // Exclude transitive dependency !!!
        exclude( group = "commons-collections", module = "commons-collections" 
    }


Multiples stages to build an application (Compiling, Testing, Running)

12. Java plugin


plugins {
    id 'java'
}

Included Tasks:
1. compileJava: generates .class files in the build directory. ./gradlew compileJava .
2. processResources: copies resources into build directory. ./gradlew processResources  .
3. jar: adds compiled classes and resources to .jar archive. The name of the file is 
<project-name>-<version.jar./gradlew jar  .
4. test: compiles, processes test resources and run tests. Test report in buid directory. ./gradlew processResources  .

Type of tasks:
action task: performs an action
aggregate task: groups other tasks

Tasks to execute with ./gradlew:
clean: removes the build directory.
assemble & jar: Compile, assemble resources and build jar. (build/(classes,libs,resources/nain)
check & test: assemble task+ assemble test classes and resources and run tests (resources/test, report)
build: assemble task+ test task


Define dependencies:
Defines the configuration used to generate classpath.
Classpaths: compile and runtime 
Keywords:
    compileOnly -> compile path (e.g. "servlet-api" the implementation is supplied by the server)
    runtimeOnly -> runtime path (e.g. to connect to a DB,  you don't need to know DB internals )
    implementation -> both compile and runtime
    testCompileOnly, test implementation, testRuntime: similar for tests

Annotation processor: like Lombok or MapStruct 

Extra properties for dependencies:
For not repeating versions  use "ext" for defining a property and double quotes for using it

ext {
    jujVersion = '5.7.2' //Define version
}

dependencies {
    //Use double quotes!!
    testImplementation "org.junit.jupiter:junit-jupiter-api:$jujVersion"
    testImplementation "org.junit.jupiter:junit-jupiter-params:$jujVersion"
    testRuntimeOnly "org.junit.jupiter:junit-engine-api:$jujVersion"
} 

Project default layout:
src/main/java      >>> build/classes/java/main
src/main/resources >>> build/resources/main
src/test/java      >>> build/classes/java/test
src/test/resources >>> build/resources/test

The jar file is saved in   build/libs

Execute the application:
java -jar <jar-file-locatiom> parameter  

Selecting the main class in the jar task:

tasks.name ('jar') {
    manifest {
        attributes('Main-Class': 'org.ximo.MyMainClass')
    }
}


tasks.named('jar') {
    manifest {
        attributes('Main-Class': 'com.gradlehero.themepark.RideStatusService')
    }
}

Class types for tasks:
-  compileJava and compileTestJava are of type JavaCompile class
- processResources and processTestResources are of type Copy class
- jar is of type Jar class

Making changes in tasks (by name or by class): 
It can be made using 2 options:
- tasks.named('taskName')   That affects only to the task with "taskName"
- tasks.withType(taskType).configureEach  Affecting all the tasks of "taskType"


 //The tasks compileJava and compileTestJava use JavaCompile Class
tasks.named('compileJava') {
    options.verbose = true   //For verbose results
}
tasks.named('compileTestJava') {
    options.verbose = true   //For verbose results
}
 //The above can be simplified as
tasks.withType('JavaCompile') { options.verbose = true }

 //==========================================================
 //The tasks processResources and processTestesources use Copy Class
tasks.named('processResources') {logflume
    include '**/*.txt'  //Only copies resources that ends with ".txt"
}

 //==========================================================
 //The task jar usee Jar Class
tasks.named('jar') {
    archiveFileName = 'myJar.jar''  //Sets the jar name
}


Make sure that you have enabled tests: 
tasks.withType(Test).configureEach {
useJUnitPlatform()
}

Example of java test class:

public class RideStatusServiceTest {

    //For displaying results:
    @ParameterizedTest(name = "{index} gets {0} ride status")
    //For executing the test with these parameter each time
    @ValueSource(strings = {"rollercoaster", "logflume", "teacups"})
    //And applies to this test
    public void getsRideStatus(String ride) {
        RideStatusService rideStatusService = new RideStatusService();
        String rideStatus = rideStatusService.getRideStatus(ride);
        assertNotNull(rideStatus);
    }

    @Test
    public void unknownRideCausesFailure() {
        RideStatusService rideStatusService = new RideStatusService();
        //An exception must be raised if the parameter is not good!
        assertThrows(IllegalArgumentException.class, () -> {
            rideStatusService.getRideStatus("dodgems");
        });
    }
}

Saving project to git:
git init                 Initialises
git add .     If you don't execute it, Git does not save anything!!!
git status
git commit -m "Create Java Project for all of you."


13. Application plugin (run java program)


plugins {
    id 'application'
}

Included Tasks:
1. run: executes the java application. ./gradlew run .

Defining the main class:
It has been defined previously, in the "jar" task how to set the "main" class, but in the application plugin, it is defined ALSO in the "application" section

aplication {
    mainClass 'org.ximo.MyMainClass'
}

tasks.name ('jar') {
    manifest {
        attributes('Main-Class': 'org.ximo.MyMainClass')
    }
}

Running the application with parameters creating a new task:
Execute the "run" task and don't forget to pass parameters! Use --args   
In Eclipse you should run the application in a terminal using  ./gradlew run --args myParameter .
I have not been able to add arguments to gradle run task in the gradle task window in Eclipse!!!. 

Tom Gregory suggests adding arguments by updating the "javaExec" task in the build.gradle file, redefining the jar of the application and the runtime jars and also the main class!!!

tasks.register('runJar', JavaExec) {// Many redundances !!!!!
    group 'My Own Tasks'    // show tasks grouped when using "gradle tasks"
    description 'execute with arguments'
    // Uses the ouput from jar task (only the outputs
classpath tasks.named('jar').map { it.outputs } // collects main jar
    classpath configurations.runtimeClasspath // collects runtime jar dependencies
    // Add arguments to the run
    args 'myParameter'
    mainClass = 'org.ximo.MyMainClass' // reenter the main class !!!
}

Debugging the application using 2 ways:
1. executing  ./gradlew runJar --debug-jvm   and open IDE on port 5005 (this can be tricky as the java compiler of gradlew and project must be the same. Maybe intelliJ can be better for this.



2. Debug as an application in Eclipse (but it can be tricky)


The process of debugging. Using Jupiter, JUnit, and Testng
1. Jupiter

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2'
    testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.2'
}

tasks.withType(Test).configureEach {
    useJUnitPlatform()
}
2. JUnit

dependencies {
    testImplementation 'junit:junit:4.3.12'
}

tasks.withType(Test).configureEach { // Empty
}
3. TestNG

dependencies {
    testImplementation 'org.testng.testng:7.7.0'
}

tasks.withType(Test).configureEach { 
    useTestNG()
}


Including and excluding only some classes for testing

tasks.withType(Test).configureEach {
    useJUnitPlatform()
    include '**/*MyTest*'
    exclude '**/*OtherTest*'
}

Executing a single test 
 ./gradlew test --test MyTest                                executes a class for testing
 ./gradlew test --test MyTest.someMethod           executes a method of a class for testing
 ./gradlew test --test MyTest.*someMethod*       executes matching method of a class for testing
 ./gradlew test --test org.tests.MyTest.someMethod  fully qualified name

Executing a test from zero (cleanTest option)
Only a test is executed if there have been changes, or the clean task has been previously used. If you want to execute it without making changes 
 ./gradlew cleanTest test 

Eclipse IDE executing a test method in the editor window
Right click on the method name of the source editor window and select "Run as -> JUnit Test"

13.1 JVM Test Suite plugin (Gradle>=7.3)

Applied automatically with java plugin
Each test suite has source directory and task
"test" is the default test suite for unit tests.

Define the required tests in the "testing" section, and can supply specific dependencies

testing {
    // Define the required integration tests
    suites {
        integrationTest(JvmTestSuite) {
            //Define internal dependencies
            dependencies {
                // Depends on the production code
                implementation project
            }
        }
    }
}

And is available in the tasks (verification-integrationTest)

Create a source folder for testing "integrationTest":
1. Create the folder src/integrationTest/java
2. Add the package com.gradlehero.themepark
3. Add a "source folder" and select the created folder "src/integrationTest/java"


Task check
In the the Gradle task window, in the verification folder, there is the check task that can be set to execute the integration test, used before committing code or continuous integration pipeline.

tasks.named('check') {
    dependsOn testing.suites.integrationTest
}


13.2 Java Version

Take care to have the same Java version for
1. Eclipse workspace settings
2. ./gradlew
3. build.gradle

In Eclipse, right click on the project and select "properties->Gradle" and select the convenient Java version


In gradlew, execute ./gradlew --version  and we get

to change the java version, in the terminal type

 export JAVA_HOME=<your new java path>

In the build.gradle we can set the java version as java-toolchain

java {
    toolchain {
        // Forces gradlew to use version 17 independenly of the JAVA_PATH!!!!
        // If not found this java version, gradlew DONWLOADS it
        languageVersion = JavaLanguageVersion.of(17)
    } 
}


Select another java version for executing the application:
Although we have the previous java-toolchain, we can additionally select another java version for executing

java {
    toolchain {
        // Forces gradlew to use version 17 independenly of the JAVA_PATH!!!!
        // If not found this java version, gradlew DONWLOADS it
        languageVersion = JavaLanguageVersion.of(17)
    } 
}

// Forces gradle to execute the application with Java 18 although we have
// compiled the application with version 17!!!!!
tasks.withType(JavaExec).configureEach {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(18)
    }
}



Select different java versions for different tasks:

// Compile the application with version 17!!!!!
tasks.withType(JavaCompile).configureEach {
    javaCompiler = javaToolchains.compilerFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

// Forces this test task to be executed with java 18
tasks('myTest', Test) {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(18)
    }
}

14. maven-publish plugin 


plugins {
    id 'application'
    id 'maven-publish'
}
Identifying artifacts to publish and the destination:
Specify: group, version, publications and destination repository (if repository is remote)!

// You must supply a group and a version for publishing
group 'com.gradlehero'
version '0.1.0-SNAPSHOT'

// Publishing part
publishing {
	// Select from
	publications{
		
		maven(MavenPublication) {
		    // select components to publish
			from components.java //java file created by the java task
		}
	}
	//destination
	repositories {
		maven {
			url 'https://<your remote repository>'
			credentials {
				username "myUser"
				// password set by an environment variable
				password System.env.CODEARTIFACT_AUTH_TOKEN
			}
		}
	}
}


Publishing : execute ./gradlew publish

14.1 Local maven repository

There is a local repository, so try to execute in the terminal
ls -l ~/.m2/repository

Publish to maven local using  ./gradlew publishToMavenLocal
and verify that the project has been added
ls -l ~/.m2/repository/com/gradlehero


15. Spring Boot applications

15.1 With java plugin

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web:2.5.3'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}
//=====================================
//   ThemeParkApplication
//=====================================
package com.gradlehero.themepark;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ThemeParkApplication {
    public static void main(String[] args) {
        SpringApplication.run(ThemeParkApplication.class);
    }
}

//=====================================
//   ThemeParkRide
//=====================================

package com.gradlehero.themepark;

public record ThemeParkRide(String name, String description) {
}

//=====================================
//   ThemeParkRideController
//=====================================

package com.gradlehero.themepark;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.Iterator;

@RestController
public class ThemeParkRideController {
    @GetMapping(path = "/rides")
    public Iterator<ThemeParkRide> getRides() {
        return Arrays.asList(
                new ThemeParkRide("Rollercoaster", "Train ride that speeds you along."),
                new ThemeParkRide("Log flume", "Boat ride with plenty of splashes."),
                new ThemeParkRide("Teacups", "Spinning ride in a giant tea-cup.")
        ).iterator();
    }
}

Run the application by right-clicking the ThemeParkApplication class and "run -run as java application"

If we change the java plugin with the application plugin we need to add this code to the build.gradle

application {
    mainClass = 'com.gradlehero.themepark.ThemeParkApplication'
}




15.2 With springframework plugin

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.5.3'
    // takes the version from springframework plugin
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'

}

repositories {
    mavenCentral()
}

dependencies {
    // takes the version from springframework plugin
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

Run the application with   ./gradlew bootRun

16. java-library plugin


plugins {
    id 'java-library'
}
Concepts:
- direct dependency (used explicitly by the application)
- transitive dependency (used by a dependency that is used by the application)

Dependency resolution:
- Only one version per group and name in dependencies.
- Gradle chooses the highest version of the dependency.


api vs implementation:
When creating a library, if the interface of any class or method in the created library contains any reference to a used library, then when declaring the dependency use api.
If our created library does not use any reference to the internally used library, then use implentation.
But this differentiation can be tricky in certain circumstances.

16.1 Special scenarios for dependency conflicts.

1. Simple exclude (transitive) of an unneeded dependency: We are sure that a transitive dependency of a direct dependency is not used. We can exclude it.

dependencies {
    implementation 'com.google.guava:guava:31.0.1-jre' {
        // unneeded dependency
        exclude group: 'com.google.code.findbugs', module; 'jsr305'
    }
2. Version upgrade (transitive): A transitive dependency (of a direct dependency) is buggy and a new version is available. 

dependencies {
    implementation 'com.google.guava:guava:31.0.1-jre'
    // change to a newer version not buggy. Does not work for version DOWNGRADE!!!
    constrains {
        implementation ('org.checkerframework:checker-qual:3.13.0') {
            because ('previous versions have a security vulnerability')
        }
    }
3. Version conflict (of a transitive dependency with direct dependency): A transitive dependency (of a direct dependency) conflicts with a direct dependency that has a different group and name. 
There are 2 solutions for this:
a. Using configuration-level exclude.
b. Module replacement.

dependencies {
    // uses "starter.logging"
    implementation 'org.springframework.boot:spring-boot-starter-web:2.6.2'
    // Conflicts with "starter.logging"
    implementation 'org.springframework.boot:spring-boot-starter-log4j2:2.5.2:2.6.2'
    
    // Option a. Exclude starter-logging. Never will be used!!!!
    configuration.implementation {
        exclude group: 'org.springframework.boot', module:'spring-boot-starter-logging'
    } 

    // Option b. Module replacement
    modules {
        module( 'org.springframework.boot:spring-boot-starter-logging')
            replacedBy ''org.springframework.boot:spring-boot-starter-log4j2'. 'Use Log42 instead of logback'
        }
    }

16.2 Dependencies task

Run   ./gradlew dependencies  to show all transitive dependencies tree of all direct dependencies
The omitted dependencies are marked as (*).
The not resolved dependencies are marked as (n).

Run   ./gradlew dependencies --configuration compileClasspath  to show all transitive dependencies tree of all direct dependencies during the compilation process

Run   ./gradlew dependencies --dependency org.slf4j:slf4j-api --configuration compileClasspath  to show all versions of a dependency used transitively or directly during the compilation process, without informing the library that uses it

When a dependency cannot be resolved, it is marked as FAILED.



17. Logging instead of println

17.1 logger:

There are 6 log levels: (1) Error, (2) Quiet (--quiet), (3) warn (--warn), (4) lifecicle (default), (5) info (--info), (6) debug (--debug)

first, create a task and use for instance 
logger.info 'Some data {}', System.currentTimerMillis()

where you can replace ".info" with ".debug" or any other level of log

tasks.register('logTest') {
    doLast {
        logger.info 'Some data {}', System.currenttimeMillis()
    }    
 
}
and now run it indicating the log level of "info" or a down level (e.g. debug) in order to see the messages.

./gradlew logTest --debug

17.2 Detecting errors:

1. Clear (syntax) error detected by task build:
If you write "implementatino" instead of implementation, gradle complains "Could not find method implementatino()", but does not say at what line it is, you need to search it whit an editor.

2. Error in the code of a custom tag being executed:
You can add --stacktrace option to show more info ./gradlew mytask --stacktrace and displays the line number of the error 

3. Debug out compiler options:
You can see whow your code is being compiled
./gradlew compileJava --debug | grep "Compiler arguments"

3. Log out dependency resolutions:
You can force gradle to refresh the dependencies from repository and show the latest version
./gradlew compileJava --refresh-dependencies --info

18. Password protection in build.gradle

18.1 defining and passing parameters to execute a task:

Place parameters mavenUserName and mavenPassword parameters instead of 'myUser' and 'myPassword' 

publishing {
	publications{
		maven(MavenPublication) {
			from components.java //java file created by the java task
		}
	}
	repositories {
		maven {
			url 'https://<your remote repository>'
			credentials {
                                // username and password ara parameters      
				username mavenUserName  //'myUser'
				password mavenPassword  //'myPassword'
			}
		}
	}
}

call the task as follows
./gradlew publish -PmavenUserName=myUser -PmavenPassword=myPassword


19. ProjectProperties

19.1 Passing properties for executing a task

1. On  the command line using -P 
./gradlew <task-name> -PmyPropName=myProvValue

2. As java system property using -D
./gradlew <task-name> -Dorg.gradle.project.myPropName=myProvValue

3. As environment variables
ORG_GRADLE_PROJECT_myPropName=myProvValue ./gradlew <task-name> 

4. In the file gradle.properties
myPropName=myProvValue

19.1 Accesing properties in the definition of tasks (build.maven)

1. Directly as a variable
println myPropName

2. Using property method
println project.property('myPropName')

3. Using findProperty method
println project.findProperty('myPropName')

4. Using Elvis operator to return a default value
println project.findProperty('myPropName') ?: 'default value'

5. Check if exist a property with hasProperty method
if (hasProperty('myPropName') {...






No hay comentarios :

Publicar un comentario