Thursday, January 6, 2011

Project Lombok: Creating Custom Transformations

Project Lombok aims to reduce Java boiler-plate via annotations that perform class transformations at compile time. Project Lombok comes with a decent set of transformations, but you may also want to create your own custom Lombok tranformations. In this blog, I will walk you through the process of extending Project Lombok to do a simple Hello World transformer.

What I present here is an approach that worked for me. At the moment, there are scarce few resources out there on this subject. I started by reading Nicolas Frankel's blog and a post in the Project Lombok discussion group but mostly it came down to groking the Project Lombok source code. With that disclaimer, let's dive in.


Overview

Project Lombok runs as an annotation processor. The annotation processor acts as a dispatcher that delegates to Lombok annotation handlers (this is what we're going to create). Handlers are discovered via a framework called SPI. Lombok annotation handlers declare the specific annotation that they handle. When delegating to a handler, the Lombok annotation processor provides an object representing the Abstract Syntax Tree (AST) of the annotated node (e.g. class, method, field, etc). The Lombok annotation handler is free to modify the AST by injecting new nodes such as methods, fields and expressions. After the annotation processing stage, the compiler will generate byte code from the modified AST.

Here's an overview of the compilation process and how Project Lombok fits in:


The basic classes you'll need to write are:
- Annotation class
- Eclipse handler
- Javac handler

In this example, we will create a very simple Lombok transformation that adds a helloWorld method to any class annotated as @HelloWorld. This is a trivial and useless transformation but serves as a simple starting point.

For example, given the following source code:
@HelloWorld
public class MyClass {

}


Our custom handler will transform the class to the following equivalent source:
public class MyClass {
public void helloWorld() {
System.out.println("Hello World");
}
}




Annotation class
This is the easy part. We just need to create an annotation called HelloWorld that can be applied to classes.
package lombok;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface HelloWorld {}

This simple annotation can only be applied to Types (interface, class, enum). Since we plan on adding a concrete method, this annotation should only be used on classes but @Target doesn't give us that granularity. Our annotation handler will be responsible for generating an error if @HelloWorld is used on an interface.

The annotation only needs to be retained in the source because we're only using the annotation to generate a method during compilation. The annotation is not needed at runtime.

Handler Overview

The handler class will be responsible for creating the AST that represents the helloWorld method and then injecting it into the AST of the Class declaration.

A simplified AST for a Class (type) declaration looks something like this:


Our handler will be adding a new Method Declaration to the Type Declaration. A Method Declaration is composed of several components. The AST for our Method Declration will have this form:


Filling in the above template with our implementation specifics, the AST starts to resemble something that looks more like Java:


Writing the Handler class

Since AST modifications are compiler specific, we'll need to provide implementations for both Javac and Eclipse. If Intellij supported Lombok, we'd likely have to provide a 3rd implementation. Since NetBeans uses Javac, when we're done we'll be able to compile using commandline javac (ant and maven included), NetBeans and Eclipse.

Both the Javac and Eclipse handlers must use "lombok" as the top-level package. In order for our handler to be discovered by the Lombok annotation processor, the Eclipse handler must be annotated as @ProviderFor(EclipseAnnotationHandler.class) and implement the EclipseAnnotationHandler interface. Likewise the Javac handler must be annotated as @ProviderFor(JavacAnnotationHandler.class) and implement the JavacAnnotationHandler interface.

Here is the starting point for our Eclipse handler:
package lombok.eclipse.handlers;
import lombok.HelloWorld;
import lombok.core.AnnotationValues;
import lombok.eclipse.EclipseAnnotationHandler;
import lombok.eclipse.EclipseNode;
import org.mangosdk.spi.ProviderFor

@ProviderFor(EclipseAnnotationHandler.class)
public class HandleHelloWorld implements EclipseAnnotationHandler<HelloWorld> {

@Override
public boolean handle(AnnotationValues<HelloWorld> annotation, Annotation ast,
EclipseNode annotationNode) {
// our logic here
}
}



Here is the starting point for our Javac handler:
package lombok.javac.handler;

import lombok.HelloWorld;
import lombok.core.AnnotationValues;
import lombok.javac.JavacAnnotationHandler;
import lombok.javac.JavacNode;
import org.mangosdk.spi.ProviderFor

@ProviderFor(JavacAnnotationHandler.class)
@SuppressWarnings("restriction")
public class HandleHelloWorld implements JavacAnnotationHandler<HelloWorld>{

public boolean handle(AnnotationValues<HelloWorld> annotation, JCAnnotation ast,
JavacNode annotationNode) {
// logic here
}
}


Handle Logic

Our handle method will need to do the following:

  1. Mark annotation as processed (Javac only)

  2. Create the helloWorld method

  3. Inject the helloWorld method into the AST of the annotated class



Our handle method looks very similar for both Eclipse and Javac.

Eclipse version:
@Override
public boolean handle(AnnotationValues<HelloWorld> annotation, Annotation ast,
EclipseNode annotationNode) {
EclipseNode typeNode = annotationNode.up();

MethodDeclaration helloWorldMethod =
createHelloWorld(typeNode, annotationNode, annotationNode.get(), ast);

EclipseHandlerUtil.injectMethod(typeNode, helloWorldMethod);

return true;
}


Javac version:
@Override public boolean handle(AnnotationValues<HelloWorld> annotation, JCAnnotation ast,
JavacNode annotationNode) {
JavacHandlerUtil.markAnnotationAsProcessed(annotationNode, HelloWorld.class);
JavacNode typeNode = annotationNode.up();

JCMethodDecl helloWorldMethod = createHelloWorld(typeNode);

JavacHandlerUtil.injectMethod(typeNode, helloWorldMethod);
return true;
}


Lombok provides our handler with the AST of the annotation node. We know that the annotation can only be applied to a Type. To get the AST for the Type (Class), we call annotationNode.up(). The annotation is a child of the Type AST, so by calling up() we get the parent AST which is the Type AST we need to modify. For simplicity, I've omitted the logic to check that the Type is actually a Class.

Next we create the node representing createHelloWorld method node. We still need to write this method which we'll look at in the next section. Once we've created the method method, we inject it the AST of our Type. This is accomplished by Lombok utility classes JavacHandlerUtil and EclipseHandlerUtil. We'll also be using these utility classes to help implement the createHelloWorld method.

Creating the helloWorld Method

Now we get the crux of the problem: creating the helloWorld method. The basic recipe will be the same for both Javac and Eclipse:

  • Start with a method node.
  • Add the return type, parameters, access level, throw clause, etc to the method node.

  • Create an expression statement to represent System.out.println("Hello World")

  • Add the expression to the method node.


Implementing this logic is by far the hardest part. We'll need to figure out how to programatically create the various AST objects. To really grok this code, you'll need to look at the Java source for the various classes in the Javac and Eclipse syntax tree packages. You'll see that Eclipse and Javac implementations differ drastically.

Here is the Javac implementation



private JCMethodDecl createHelloWorld(JavacNode type) {
TreeMaker treeMaker = type.getTreeMaker();

JCModifiers modifiers = treeMaker.Modifiers(Modifier.PUBLIC);
List<JCTypeParameter> methodGenericTypes = List.<JCTypeParameter>nil();
JCExpression methodType = treeMaker.TypeIdent(TypeTags.VOID);
Name methodName = type.toName("helloWorld");
List<JCVariableDecl> methodParameters = List.<JCVariableDecl>nil();
List<JCExpression> methodThrows = List.<JCExpression>nil();

JCExpression printlnMethod =
JavacHandlerUtil.chainDots(treeMaker, type, "System", "out", "println");
List<JCExpression> printlnArgs = List.<JCExpression>of(treeMaker.Literal("hello world"));
JCMethodInvocation printlnInvocation =
treeMaker.Apply(List.<JCExpression>nil(), printlnMethod, printlnArgs);
JCBlock methodBody =
treeMaker.Block(0, List.<JCStatement>of(treeMaker.Exec(printlnInvocation)));

JCExpression defaultValue = null;

return treeMaker.MethodDef(
modifiers,
methodName,
methodType,
methodGenericTypes,
methodParameters,
methodThrows,
methodBody,
defaultValue
);
}


With Javac, we need to generate an object for all parts of the method: modifiers, generic types, return type, method name, parameters, throw clause, and method body. Creating these object is via a TreeMaker class that is part of Javac. TreeMaker is a factory class for creating all the different types of nodes. The method body is comprised of various nodes as well: method reference to System.out.println, arguments to println which includes the String literal "hello world". It's all tied together as a method invocation of the println method reference. Finally we use treeMaker to combine all the pieces into a method defintion.

Now let's look at the Eclipse implementation:
private MethodDeclaration createHelloWorld(EclipseNode typeNode, EclipseNode errorNode, ASTNode astNode, Annotation source) {
TypeDeclaration typeDecl = (TypeDeclaration) typeNode.get();

MethodDeclaration method = new MethodDeclaration(typeDecl.compilationResult);
Eclipse.setGeneratedBy(method, astNode);
method.annotations = null;
method.modifiers = Modifier.PUBLIC;
method.typeParameters = null;
method.returnType = new SingleTypeReference(TypeBinding.VOID.simpleName, 0);
method.selector = "helloWorld".toCharArray();
method.arguments = null;
method.binding = null;
method.thrownExceptions = null;
method.bits |= ECLIPSE_DO_NOT_TOUCH_FLAG;

NameReference systemOutReference = createNameReference("System.out", source);
Expression [] printlnArguments = new Expression[] {
new StringLiteral("Hello World".toCharArray(), astNode.sourceStart, astNode.sourceEnd, 0)
};

MessageSend printlnInvocation = new MessageSend();
printlnInvocation.arguments = printlnArguments;
printlnInvocation.receiver = systemOutReference;
printlnInvocation.selector = "println".toCharArray();
Eclipse.setGeneratedBy(printlnInvocation, source);

method.bodyStart = method.declarationSourceStart = method.sourceStart = astNode.sourceStart;
method.bodyEnd = method.declarationSourceEnd = method.sourceEnd = astNode.sourceEnd;
method.statements = new Statement[] { printlnInvocation };
return method;
}


With the Eclipse implementation, instead of using a TreeMaker to create the various components, we just create the components via their constructors and attach child nodes to parents via property assignments. Eclipse developers apparently aren't big on encapsulation. They also seem to be fixated on using char arrays instead of Strings. Another thing we need to deal with is telling Eclipse that the HelloWorld annotation was responsible for generating the method. That way if there is an error with our generated method, Eclipse will associate the error with @HelloWorld annotation.

The complete source along with a Maven project setup can be found here

Going beyond HelloWorld

Hopefully by now the architecture of Lombok is a little clearer. Once you've mastered Hello World you're ready to start creating more useful transformations. As you would expect, more interesting behavior requires more complex AST transformations. You'll probably spend a lot of time looking at the source code for the handlers that the Project Lombok maintainers have already created. This is the downside of using private APIs to generate code. You're on your own to figure out how these APIs work.

Appendix - Project Setup

Before you can build any Lombok transformations, you'll need a way to build your code.

Where to put your code

The first thing you need to decide is where your code will live. There are 2 options:
1. Fork Project Lombok source
2. Create a new source project

Let's look at both options in more detail:

Forking Lombok

By far the easiest way to get started is just to clone the Project Lombok git repo and place your custom classes alongside the core Lombok classes. This is going to be the fastest and easiest way to get started. You won't have to deal with configuring library dependencies or patching Eclipse. Down the road you can easily move to a standalone project for your custom classes.

The downside is that you are forking the project is that you'll have to adopt Lombok's project structure and build system (Ant+Ivy). Using ant, you can generate an Eclipse project which makes things easy.

Cloning Project Lombok is simple. You'll need to have git installed. Then run the command:
git clone https://github.com/rzwitserloot/lombok.git


Navigating source project

The top-level lombok directory contains a build.xml.

To compile and build the lombok jar run: ant
To generate an eclipse project run: ant eclipse

The lombok jar is generated under the dist subdirectory. Once you've added your custom handlers, you'll need to patch Eclipse with your updated jar by running: java -jar <new lombok jar>:

Source code is organized into the following directories:
src/core/lombok - Annotations
src/core/lombok/eclipse/handlers - Eclipse annotation handlers
src/core/lombok/javac/handlers - Javac annotation handlers

Creating a stand-alone project

Skip this section if you chose to fork Project Lombok.

Instead of forking the Project Lombok repo, another more complicated approach is to place your custom Lombok extensions in a separate standalone project.

Setting up custom project comes with some pain though. You'll need to handle dependency management and Eclipse patching yourself.

At a minimum your project will need to provide the following libraries:
- Project Lombok
- SPI
- Eclipse Core JDT
- Sun's tools.jar (this contains javac classes)

The example I provide here uses Maven2. This presents a challenge because SPI library does not have a public maven repo (that I could find), nor does Eclipse Core JDT or tools.jar. So these have to be installed manually into your local repo.

The easiest way to obtain theses jars is by checking out Project Lombok and doing a build (see above). The build will download dependent libraries under lib/build. The lombok dist jar can be found under the dist/. With Maven, you'll need to install these jars these jars in your local repo.

Download the Maven project for the HelloWorld project here

Once you've built the jar, you'll need to patch it manually into Eclipse. Copy the jar to the same folder as your eclipse.ini. Edit eclipse.ini, and add your jar to the bootclasspath. Here is an example for adding the hello-lombok.jar from the example project to eclipse.ini.


...
-javaagent:lombok.jar
-Xbootclasspath/a:lombok.jar
-Xbootclasspath/a:hello-lombok.jar
...

64 comments:

  1. There seems to be a copy of the jdt project in maven.
    <dependency>
    <groupId>org.eclipse.jdt</groupId>
    <artifactId>core</artifactId>
    <version>3.3.0-v_771</version>
    <scope>provided</scope>
    </dependency>
    This seems to let you depend on tools.jar if ${java.home}/../ is the jdk.
    <dependency>
    <groupId>sun.jdk</groupId>
    <artifactId>tools</artifactId>
    <version>1.7.0</version>
    <scope>system</scope>
    <systemPath>${java.home}/../lib/tools.jar</systemPath>
    </dependency>
    I still have to install the SPI project directly but that's not too hard.

    ReplyDelete
    Replies
    1. Thanks for the post, I am techno savvy. I believe you hit the nail right on the head. I am highly impressed with your blog. It is very nicely explained. Your article adds best knowledge to our Java EE Training in Chennai. or learn thru Java EE Training in Chennai Students.

      Delete
  2. Great!! This is one of the coolest thing I saw until now!!

    ReplyDelete
  3. Thanks for your blog which helps me a lot now.

    An idea is that generating @Autowired fields for classes with an annotation like "@SpringAutowiredBeans".

    @SpringAutowiredBeans({
    UserService.class
    })
    public class Controller {
    }

    gets

    public class Controller {
    private UserService userService;
    }

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. I was wondering if its possible to do something like a Oracle Sequence Generator Id annotation like that:

    @OracleSequenceId("xxx_seq_id")
    private Integer Id;

    and translate it to something like:

    @Id
    @SequenceGenerator(name="idSeq", sequenceName="xxx_seq_id")
    @GeneratedValue(...)
    private Integer id;

    ReplyDelete
  6. Great article, thanks! I think there is a little mistake in your source code in hello-lombok.zip. notAClass method has a following condition:
    boolean notAClass = typeDecl == null || (flags & (Flags.INTERFACE | Flags.ENUM | Flags.ANNOTATION)) == 0;
    It appears that condition should be NOT equals zero, because in case of class 'flags' field equals to 1

    ReplyDelete
  7. It's interesting that many of the bloggers to helped clarify a few things for me as well as giving.Most of ideas can be nice content.The people to give them a good shake to get your point and across the command
    Data Science training in Chennai
    Data science training in Bangalore
    Data science training in pune
    Data science online training
    Data Science Interview questions and answers
    Data Science Tutorial
    Data science training in bangalore

    ReplyDelete
  8. This comment has been removed by the author.

    ReplyDelete
  9. I am impressed by the information that you have on this blog. It shows how well you understand the java. Thanks for sharing

    ExcelR Data Science

    ReplyDelete
  10. I really enjoy simply reading all of your weblogs. Simply wanted to inform you that you have people like me who appreciate your work. Definitely a great post. Hats off to you! The information that you have provided is very helpful.



    DATA SCIENCE COURSE

    ReplyDelete
  11. I finally found great post here.I will get back here. I just added your blog to my bookmark sites. thanks.Quality posts is the crucial to invite the visitors to visit the web page, that's what this web page is providing.
    date analytics certification training courses
    data science courses training
    data analytics certification courses in Bangalore

    ReplyDelete
  12. Such a wonderful blog on Machine learning . Your blog have almost full information about Machine learning .Your content covered full topics of Machine learning that it cover from basic to higher level content of Machine learning . Requesting you to please keep updating the data about Machine learning in upcoming time if there is some addition.
    Thanks and Regards,
    Machine learning tuition in chennai
    Machine learning workshops in chennai
    Machine learning training with certification in chennai

    ReplyDelete
  13. Such a wonderful blog on Machine learning . Your blog have almost full information about Machine learning .Your content covered full topics of Machine learning that it cover from basic to higher level content of Machine learning . Requesting you to please keep updating the data about Machine learning in upcoming time if there is some addition.
    Thanks and Regards,
    Machine learning tuition in chennai
    Machine learning workshops in chennai
    Machine learning training with certification in chennai

    ReplyDelete
  14. The material and aggregation is excellent and telltale as comfortably. Data Science Course in Pune

    ReplyDelete
  15. It should be noted that whilst ordering papers for sale at paper writing service, you can get unkind attitude. In case you feel that the bureau is trying to cheat you, don't buy term paper from it.
    what are solar panel and how to select best one
    learn about iphone X
    top 7 best washing machine
    iphone XR vs XS max




    ReplyDelete
  16. Thank you for sharing the information. It was exactly nice. We fix all Xero Issue like Xero Accounting Software, Payroll, Invoice, vat, Cloud etc then dial Xero Technical Support Helpline Number and get Instant online Support and Services from Xero.
    Xero Support Number

    ReplyDelete
  17. QuickBooks Enterprise is a flexible and reliable accounting software developed by Intuit. Enterprise solutions are mainly designed for the small and mid-size businesses so that they can access on-premises accounting software’s as well as cloud-based editions while looking to manage and pay bills, receive business payments, and maintaining payroll records properly. QuickBooks Enterprise support is essential for all those organizations that have installed this specific software to handle multiple users, locations, large volume or high stock of business data but are facing difficulty in upgrading them from time to time. Visit : Quickbooks enterprise support

    ReplyDelete
  18. I like viewing web sites which comprehend the price of delivering the excellent useful resource free of charge. I truly adored reading your posting. Thank you!
    www.technewworld.in
    How to Start A blog 2019
    Eid AL ADHA

    ReplyDelete
  19. I have to search sites with relevant information on given topic and provide them to teacher our opinion and the article.
    big data course malaysia

    ReplyDelete
  20. Quickbooks enterprise support Phone number
    Get 24-hour support for corporate Quickbooks by contacting the QuickBooks Enterprise support phone number. We are ready to solve the QuickBooks Enterprise problems through a certified QuickBooks Enterprise support group. Call our Quickbooks support team at +1 (833) 400-1001 and contact our certified QuickBooks specialist for help.

    ReplyDelete
  21. Quickbooks enterprise support number
    +1 (833) 400-1001 is available to solve QuickBooks Enterprise problems through QuickBooks Enterprise support. Call our Quickbooks support team at +1 (833) 400-1001 and contact our certified QuickBooks specialist for help.

    ReplyDelete
  22. Quickbooks enterprise support
    Solve QuickBooks Enterprise problems using the QuickBooks Enterprise support team. Call our Quickbooks support team at +1 (833) 400-1001 and contact our certified QuickBooks specialist for help.

    ReplyDelete
  23. Really nice and interesting post. I was looking for this kind of information and enjoyed reading this one. Keep posting. Thanks for sharing.
    Data Science Courses

    ReplyDelete
  24. QuickBooks Payroll has emerged the best accounting software that has had changed the meaning of payroll. Phone Number for QuickBooks Payroll Support could be the team that provide you Quickbooks Payroll Support. This software of QuickBooks comes with various versions and sub versions. Online Payroll and Payroll for Desktop may be the two major versions and they are further bifurcated into sub versions. Enhanced Payroll and Full-service payroll are encompassed in Online Payroll whereas Basic, Enhanced and Assisted Payroll come under Payroll for Desktop.

    ReplyDelete
  25. very informative article

    data science course singapore is the best data science course

    ReplyDelete
  26. Find a local DJ, DJ wanted London
    Dj Required has been setup by a mixed group of London’s finest Dj’s, a top photographer and cameraman. Together we take on Dj’s, Photographers and Cameramen with skills and the ability required to entertain and provide the best quality service and end product. We supply Bars, Clubs and Pubs with Dj’s, Photographers, and Cameramen. We also supply for private hire and other Occasions. Our Dj’s, Photographers and Cameramen of your choice, we have handpicked the people we work with

    ReplyDelete
  27. You will get an introduction to the Python programming language and understand the importance of it. How to download and work with Python along with all the basics of Anaconda will be taught. You will also get a clear idea of downloading the various Python libraries and how to use them.
    Topics
    About Excelr Solutions and Innodatatics
    Introduction to Python
    Installation of Anaconda Python
    Difference between Python2 and Python3
    Python Environment
    Operators
    Identifiers
    Exception Handling (Error Handling)

    [url=https://www.excelr.com/data-science-certification-course-training-in-singapore]Excelr Solutions[/url]

    ReplyDelete
  28. Buy Tramadol Online from the Leading online Tramadol dispensary. Buy Tramadol 50mg at cheap price Legally. Buying Tramadol Online is very simple and easy today. Shop Now.

    ReplyDelete
  29. PhenQ_Reviews 2019 – WHAT IS PhenQ ?


    How_to_use_PhenQ ?This is a powerful slimming formula made by combining the multiple weight loss
    benefits of variousPhenQ_ingredients. All these are conveniently contained in
    one pill. It helps you get the kind of body that you need. The ingredients of
    the pill are from natural sources so you don’t have to worry much about the side
    effects that come with other types of dieting pills.Is_PhenQ_safe ? yes this is completly safe.
    Where_to_buy_PhenQ ? you can order online.PhenQ Scam ? this medicine is not scam at all.


    Watch this PhenQ_Reviews to know more.
    Know about PhenQ Scam from here.
    know Is_PhenQ_safe for health.
    you don`t know How_to_use_PhenQ check this site

    wanna buy phenq check this site and know Where_to_buy_PhenQ and how to use.

    check the PhenQ_ingredients to know more.

    what is PhenQ check this site.

    ReplyDelete
  30. Hp Printer Contact Support is the leading HP Printer Support service provider that dedicatedly focuses on delivering World class technical support to the customers owing printers, scanners, Photocopy machine and similar plotters. We own the technical expertise in handling the growing demand of expert Printer Support services across the world. We have a team of technical experts who provide you instant HP printer support to you to help you in resolving your issues with the printers.We are committed to bridging the gap between the printer problems and respective solutions by providing a best of online technical support services that are readily available round the clock. We adhere to serve our customers with the high quality and result oriented printer repair service that will depart them with complete satisfaction for printer use. In this run of our services, we thus acquired a lot of trust and support from our clients and customers.
    With our HP printer support guidance, you can clear out all your queries regarding printer issues and make its uses for future also. You just need to call us on ourHP Printer Support Phone Number +1-855-381-2666 that is made toll-free for you and you can able to connect with us anytime.

    ReplyDelete
  31. Download latest audio and video file fromvidmate

    ReplyDelete
  32. Car Maintenance Tips That You Must Follow


    For everyone who owns it, Car Maintenance Tips need to know.
    Where the vehicle is currently needed by everyone in the world to
    facilitate work or to be stylish.
    You certainly want the vehicle you have always been in maximum
    performance. It would be very annoying if your vehicle isn’t even
    comfortable when driving.
    Therefore to avoid this you need to know Vehicle Maintenance Tips or Car Tips
    Buy New Car visit this site to know more.

    wanna Buy New Car visit this site.
    you dont know about Car Maintenance see in this site.
    wanna know about Car Tips click here.
    know more about Hot car news in here.


    ReplyDelete
  33. It’s been an incredible Journey with UNIMAS for the last few years and we are proud that we are recognized as their
    Data Science Training training delivery partner.
    ExcelR successfully handled many batches in this university.
    With our quality delivery and top notch brand reputation, UNIMAS accredited our training program and recognized ExcelR as an official partner to deliver various certification
    programs through their university. Avail globally recognized certification from UNIMAS by completing Data Analytics course in ExcelR.



    Data Science Training

    keywords:data science training,data science course

    ReplyDelete
  34. Thanks for the detailed informative blog post. I find it to be an intense read but filled with solutions. www.ezcourtpay.com

    ReplyDelete
  35. where you can buy tramadol online? drungo is the right place for purchasing tramadol online.

    ReplyDelete
  36. Sehar News is a wide area that envelops pakistan news , kashmir news , International News, Sports News, Arts and
    Entertainment News, Science and Technology, Business News, latest news in urdu , Education News and today news Columns.
    The perusers can snatch most recent urdu news dependent on different political and get-together
    occurring in the nation. Sehar News covers the most recent and up and coming news features, Read today urdu news and top stories from different backgrounds and carries it to the viewers



    wanna know latest pakistan news ? click pakistan news and know more.

    Read latest news in urdu and know more .

    read all the latest urdu news in this site.

    you dont know ? about today news click here and know more.

    know the current news of kashmir news check here.

    read all about today urdu news and gain knowledge.

    ReplyDelete
  37. LogoSkill, Logo Design Company is specifically a place where plain ideas converted into astonishing and amazing designs. You buy a logo design, we feel proud in envisioning
    our client’s vision to represent their business in the logo design, and this makes us unique among all. Based in USA we are the best logo design, website design and stationary
    design company along with the flayer for digital marketing expertise in social media, PPC, design consultancy for SMEs, Start-ups, and for individuals like youtubers, bloggers
    and influencers. We are the logo design company, developers, marketers and business consultants having enrich years of experience in their fields. With our award winning
    customer support we assure that, you are in the hands of expert designers and developers who carry the soul of an artist who deliver only the best.

    Logo Design Company

    ReplyDelete
  38. LogoSkill, Custom Logo Design Services
    is specifically a place where plain ideas converted into astonishing and amazing designs. You buy a logo design, we feel proud in envisioning
    our client’s vision to represent their business in the logo design, and this makes us unique among all. Based in USA we are the best logo design, website design and stationary
    design company along with the flayer for digital marketing expertise in social media, PPC, design consultancy for SMEs, Start-ups, and for individuals like youtubers, bloggers
    and influencers. We are the logo design company, developers, marketers and business consultants having enrich years of experience in their fields. With our award winning
    customer support we assure that, you are in the hands of expert designers and developers who carry the soul of an artist who deliver only the best.

    Custom Logo Design Services

    ReplyDelete
  39. We at Strive 2 drive,driving school In Melbourne.
    is one of the best & safe driving school where you have an ease of access
    to a wide array of special driving features. We are focused at your
    comfort and so we have put together facilities within the site to ensure Driving School in Melbourne!
    that you get the very best.Driving School in Melbourne!

    ReplyDelete
  40. We at Strive 2 drive,driving school In Melbourne. Driving School in Melbourne!
    is one of the best & safe driving school where you have an ease of access
    to a wide array of special driving features. We are focused at your
    comfort and so we have put together facilities within the site to ensure
    that you get the very best. Driving School in Melbourne!

    ReplyDelete
  41. This comment has been removed by the author.

    ReplyDelete
  42. Unique clipping path and high quality image editing service Company in Qatar.We are offering Ecommerce product and all image editing service with reasonable price.See more Details visit here: Clipping Path

    ReplyDelete