Вы находитесь на странице: 1из 85

Groovy Workshop

Customer Experience Solution Consulting


Chris Tomkins chris.tomkins@oracle.com
Tim Bennett - tim.j.bennett@oracle.com

Fusion Apps Developer Relations


Richard Bingham richard.bingham@oracle.com

17th June, 2015

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 1


Safe Harbor Statement
The following is intended to outline our general product direction. It is intended for
information purposes only, and may not be incorporated into any contract. It is not a
commitment to deliver any material, code, or functionality, and should not be relied upon
in making purchasing decisions. The development, release, and timing of any features or
functionality described for Oracles products remains at the sole discretion of Oracle.

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 2


Help you deliver Help you become
customer success self sufficient

Objectives
Learn from Forge better
your experiences relationships

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 3


Name
Company
Experience with Sales Cloud/Groovy
Fascinating fact about yourself

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 4


Used Sales Cloud/ Basic programming
Application Composer skills

Assumptions
Access to an Creativity knows
environment no bounds

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 5


Agenda
Course Goals and Objectives
The when + where + how of Groovy in Sales Cloud

An Architecture Overview
Event Architecture
Data Architecture
Demo Worked Examples
From our Master List
Hands-on Workshop
You Try The Master List

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 6


So What Is Groovy?
http://groovy-lang.org/
Runs server-side on the JVM
Java-like syntax and features
Application Composer Groovy
Intended for short scripts
Coding Palette provided
No source control / IDE-connection
Underlying libraries are restricted

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 7


Accessing App Composer

https://<hostname>/crmCommon/faces/ExtnConfiguratorHome

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 8


The Groovy Palette

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 9


A Simple logger

def messageText = "DEBUG: " + now() + " - " + message adf.util.WriteLog("${RecordName}, After Create")

println(messageText)

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 10


Viewing println() statements
Use Query By Example to find messages:

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 11


Event Architecture

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 12


Outline
Where you can put Groovy

Logger function with variable levels

Custom Object
Controller field to trigger different events

Observe!

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 13


Where you can put Groovy
Fields
Required
Updateable
Default Value
Formula Fields

Actions & Links


Buttons
Links

Web Content

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 14


Where you can put Groovy
Server Scripts
Object Rules
Field Rules
Object Triggers
Field Triggers
Object Functions
Global Functions
Object Workflow Conditions
Dynamic Layout Conditions
No support for creating custom events or run via ESS (yet)
Copyright 2015, Oracle and/or its affiliates. All rights reserved. 15
Transaction Lifecycle Demonstration/Discussion
Custom Object with 4 levels of logging
All Object triggers
Validation Rules Object and Field
Field triggers
Everything else
Object Workflows
Calculated fields
Required
adf.util.WriteLog("${RecordName}, OT After Create", 1)
Updateable
if (F2_c == 'CREATE') {
etc adf.util.WriteLog("${RecordName}, OT After Create Validation", 1)
adf.error.raise(null)
}

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 16


Fields

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 17


Action and Links

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 18


Validation Rules

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 19


Object Triggers

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 20


Example 1 Create with Object Triggers only

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 21


Basic Create Events

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 22


User presses Cancel instead of Save

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 23


Modify record notice the time difference
Invalidate/Modify

Save

Save

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 24


Delete Record

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 25


Example 2 Create with Object Triggers, Validation rules

Field validation fires only if the field is set

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 26


Example 2 - Modify in 3 different ways

The order of some events may change


depending on what your user does!

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 27


Create versus Modify
Create Modify

After Changes Posted is the last event where the current record can be changed within in the current commit cycle

It is also the only event common to Create and Modify that is inside the current commit cycle

Many projects duplicate logic between Create and Modify

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 28


Object Modification during Save Cycle (setAttribute)

After Changes Posted

Object Validation

After Update

Before Update

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 29


Add in Field Triggers trigger on F3

F3 not changed F3 changed twice

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 30


Add Everything Else how many log entries?

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 31


Events generated by Table display

24 events

4 for each displayed record


1 Required expression
2 x Calculated expression (same field)
Link

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 32


Modify Events

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 33


Summary
Be aware that lots of events can fire repeatedly
Be careful about what you put in noisy events
Never use setAttribute without first checking whether the action has
already happened
Never modify the current record using Object workflow
Only call code when it needs to be called

https://blogs.oracle.com/fadevrel/entry/groovy_performance_series_gene
ral_recommendations

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 34


Data Architecture

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 35


First: Think Like A Database Company
How To Query and Do DML in Your Scripts
Opportunity
Objects are like Database Views/Tables Row 1

They are Related to each other, like PK-FK Row 2

You Can Access One From Another Row 3


Row 4
Usually the relationship is used implicitly
Source To Target (sets up FK) Row 1
Row 2
Note: There are no one-to-ones, only 1:M and M:M Contact

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 36


KISS just use Joins
Add related fields via JOINS
Reusable VO relationships
Simply Add Fields

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 37


Object Relationships
Customer / Account

Opportunity

Custom Object ABC

Opportunity Opportunity Opportunity


Contact Revenue Revenue

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 38


Three Methods of Data Access
Accessing related object data records is possible in three ways
Use Business Object Relationships
Use the existing VOs
Fastest / Simplified
Use View Objects
Instantiating a VOs
Flexible / Query
Use Web Services
Use APIs over the VOs
External API / Transaction Control

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 39


In-Context Objects, Collections, and Related
Objects
Data Architecture

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 40


Object Relationship Types
Seeded Relationships
Standard Objects
Joins are reusable
Parent:Child
Opportunity Revenue Lines
Dynamic Choice Lists
Explicitly Created Relationships
1:M = Reference
M:M new in R9
Cascade Delete
Copyright 2015, Oracle and/or its affiliates. All rights reserved. 41
In-Context Objects, Collections, and Related Objects
All groovy runs in the context of a Business Object
Lead, Opportunity, Account etc.

You can access the fields of that Business Object directly


if !(CloseDate){ setAttribute('CloseDate',(today() + 14)) }

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 42


Hands-on Example
Common Application: Account object
Add a Link that has a Dynamic Value using an expression passing data from a field /
object field
def url = 'https://maps.googleapis.com/maps/api/staticmap?center=' +
PrimaryAddressCity + ',' + PrimaryAddressCountry +'&zoom=13&size=600x300'
return url

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 43


In-Context Objects, Collections and Related Objects
Found Between Seeded Objects
Created Between Your Custom/Seeded Objects
Which Is Available Is Based On Your Current Context and the Relationship

One To Many = Related Collection


Many To One = Related Object

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 44


Example: Custom Object (Explicit) Relationships

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 45


Hands-On Example Looking At Relationships
Browse around the following pages and just look at the Objects and
Relationships
The Relationships Page
The Groovy Palette

One To Many

Many To One

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 46


In-Context Objects, Collections, and Related Objects
Result of a One-To-Many Relationship
For example Custom Child Objects, From Source To Target Custom Objects
Data Access for Related Collections
Get access using the Collections Accessor
Collection Name is listed in Palette (new ones end [ObjectName_c])
Creates a ROW ITERATOR Object
Always assign to a variable for use, then use methods: hasNext(), first(), next(), reset()
def myRevenue = nvl(ChildRevenue,0);
If (myRevenue) {
def myLine = myRevenue.first();
myLine.setAttribute('UnitPrice', '5');
}

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 47


In-Context Objects, Collections, and Related Objects
You CANNOT use the INSERT button on the Palette
You must use the Collections Accessor

def myR = OpportunityContact;


If(myR){
def myLine = myR.first();
def txt = myLine.PartyName
setAttribute('CustomLongText_c', txt) }

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 48


In-Context Objects, Collections, and Related Objects
Child Custom Objects Collection Name

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 49


In-Context Objects, Collections, and Related Objects
One To Many means you may have multiple records
Check if you have more than one row:
def resultCount = Obj.size()
Iterate over each object row (e.g. Opty Revenue lines)
While loop using: Obj.hasNext()
Go to the first row: def oneRow = Obj.first()
Iterating the record: Obj.next()

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 50


Hands-on Example Using The Collection Accessor
Show a field value from the Child Object when hitting a button
Show OpportunityContact Field from an Opportunity button press

def myR = nvl(OpportunityContact,0);


def myLine = myR.first();
def txt = myLine.PartyName
setAttribute('CustomLongText_c', txt)

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 51


In-Context Objects, Collections, and Related Objects
Result of One-To-Many looking back from Target Towards Source
Revenue Lines up to Opportunity
Target Custom Objects up to Source Object
Data Access for Related Objects
Simply Referenced using Object dot Field
Always Use the null-safe dot notation (?.)
Examples
def contAdd = Person?.PrimaryFormattedAddress
Accessing the related Organization object field from an Opportunity script
If(Organization?.Level_c==Platinum){ setAttribute(OpptyTravelBudget_c,APPROVED) }

Here you CAN use the INSERT button on the Palette


Copyright 2015, Oracle and/or its affiliates. All rights reserved. 52
Hands-On Example Related Objects
Show a field value from the Parent Object when creating a Child record
Opportunity Field from create Opportunity Revenue record
throw new oracle.jbo.ValidationException("The Opportunity ID is " + Opportunity?.OptyId)

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 53


GOTCHA: Updating your Related Objects fields
setAttribute() always expects the name of a field on the CURRENT object
THIS DOESNT WORK: setAttribute('TroubleTicket_c?.Status_c', 'Open')
First you access the parent object by creating an object
def parentTicket = TroubleTicket_c
Then call the setAttribute on that parent object
parentTicket?.setAttribute('Status_c', 'Open')

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 54


GOTCHA: Accessing Related Objects From Contexts

If Accessing from Parent Context (Source) to Child (Target) Then Normal


if(Organization?.Level_c==Platinum){ setAttribute(OpptyTravelBudget_c,APPROVED) }

If Accessing from Child Context (Target) to Parent (Source) Then Use The
Accessor field: [ReferencedObjectName]_c
if(Opportunity_c.myStatus_c == 'Open') {

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 55


What About Related Custom Objects?
Created using the One-to-Many type from the Relationships page
A top-level Custom Object should be accessed using a Related Collection
A Custom CHILD object should also be accessed using a Related Collection
Look out for auto-generated Object Accessor Names

def txt = ""


def myCust = TravelPlan_OptyTravel_Tgt;
while(myCust.hasNext()){
def myline = myCust.next()
txt = txt + myline.RecordName + " "
}
setAttribute('CustomLongText_c', txt)

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 56


Using Objects related through a Dynamic Choice List
Dynamic Choice List creates a Related Object
As such use the null-safe dot notation, plus
DCLs have three significant fields for script use:
[ReferencedObjectName]_c hold the primary display value of the DCL
[ReferencedObjectName]_Id_c is the FK, holding the PK value of the referenced
object.
[ReferencedObjectName]_Obj_c the Accessor Field used to get all other fields from
the related object

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 57


Hands-On Example: Accessing other DCL Fields
Access some field values through a DCL

def MonitorEmail = nvl(MonitoringPerson_Obj_c?.EmailAddress,"no email")


def fk = MonitoringPerson_Id_c
setAttribute('CustomLongText_c', MonitorEmail +" / ID:" + fk)

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 58


GOTCHA: Setting Foreign Keys To Change Relationships
The Accessor field objects are great for Queries
What if you want to update which record is related to this record?
At this time you can only use the UI
PK / FK fields are auto-generated and NOT exposed/displayed
Were working on getting this made available for developers

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 59


Using Intersection Objects from M:M Relationships
Two One-To-Many Relationships
Accessor Fields allow direct access from Source To Target
FK Field Names are hidden

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 60


Accessing Data Using View Objects
Data Architecture

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 61


Instantiating View Objects
Sometimes
You need to use records from an Unrelated Object
You need to Query all records
You need to do CRUD operations

You can create instances of View Objects


Use the newView() function
Limited to picking VOs in the system
Includes Custom Objects

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 62


Querying View Object Data
Two Options By Key or Using A Query

Use findByKey() lookup using the record Primary Key

def partyVO = newView('PersonProfile')


def partyRow = partyVO.findByKey(key(CustomerPartyId),1)
def found = partyRow.size() == 1 ? partyRow[0] : null;
if (found != null) {
msg += "DoB: ${partyRow.DateOfBirth}\n"
}

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 63


Querying View Object View Criteria
def vo = newView('OpportunityVO')
STEPS ARE: def vc = newViewCriteria(vo)
def vcr = vc.createRow()
def vci = vcr.ensureCriteriaItem('TargetPartyName')
1. Create a View Object vci.setOperator('=')
vci.setValue("CustomerABC")

2. Create a ViewCriteria() vc.insertRow(vcr)


vo.appendViewCriteria(vc)
vo.executeQuery()

3. Create a Query Row


4. Query from: Item + Operator + Value def vo = newView('OpportunityVO');
def vc = vo.createViewCriteria();

5. Insert the Query Row (insertRow) def vcRow = vc.createViewCriteriaRow();


def vcRow1 = vc.createViewCriteriaRow();
def vc1 = vcRow.ensureCriteriaItem("Name");
6. Append the ViewCriteria vc1.setOperator("STARTSWITH");
vc1.setValue("A");
vc.insertRow(vcRow);
7. Execute on the View Object vo.appendViewCriteria(vc)
vo.executeQuery()

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 64


def vo = newView('OpportunityVO');

Querying View Object View Criteria def vc = vo.createViewCriteria();

def vcRow = vc.createViewCriteriaRow();

Multiple Query Criteria def vcRow1 = vc.createViewCriteriaRow();

def vc1 = vcRow.ensureCriteriaItem("Name");

Create Multiple Query Rows (vc1 & vc2) vc1.setOperator("STARTSWITH");

vc1.setValue("A");
Use setConjunction() def vc2 = vcRow1.ensureCriteriaItem("Name");

Insert and execute


vc2.setOperator("=");

vc2.setValue("AMMM");

vcRow.setConjunction(1);

vc.insertRow(vcRow);

vc.insertRow(vcRow1);

vo.applyViewCriteria(vc);

vo.executeQuery();

while(vo.hasNext()){

def row = vo.next()

println(row.getAttribute('Name'))

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 65


Doing CRUD on View Objects
Create = newView() + insertRow()
Update = newView() + setAttribute()
Delete = newView() + remove()

Understanding Commits
Explicitly commit is not recommended but is possible
The transaction commit is tied directly to a UI commit!
Calling commit() or rollback() will affect changes pending in the current transaction!

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 66


Hands-on Demo - VO CRUD
Add a Trigger on an Object that when fired Creates another Object record
E.g. Create an Opportunity When New Household Is Created

def voOpty = newView('OpportunityVO')


def rowOpty = voOpty.createRow();
def householdId = PartyId
rowOpty.setAttribute("Name", "Autocreated from Household " + householdId);
voOpty.insertRow(rowOpty);

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 67


Accessing Data Using Web Services
Data Architecture

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 68


Sales Cloud Web Services
SOAP not REST - yet!
Internal Services are detailed in OER
Recommended To test via SOAPUI
Pass current data in for queries etc
In Sales Cloud a consistent set of operations
get[Object] with Primary Key
find[Object] with a findCriteria
create[Object] with Required Fields
update[Object] with PK ID
delete[Object] with PK ID
Copyright 2015, Oracle and/or its affiliates. All rights reserved. 69
Data Access via Web Services
Define/Register Your Web Service First
Call it using adf.webServices.[Name].[operation]([MAP])
Listed in Palette

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 70


Data Access via Web Services - OER
http://fusionappsoer.oracle.com/oer/

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 71


Data Access via Web Services - OER
Use the SDO for payload information

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 72


Data Access via Web Services SOAP UI

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 73


Accessing Internal IDs For Testing

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 74


Using the find[Object] Operation
Requires NO INTERNAL ID VALUES (yeah!)
Service accepts two object parameters:
findCriteria a MAP object that equates to the XML payload
findControl not used but required

def findCriteria = [filter: [ group: [ [ item: [ [ attribute :'CreationDate', operator :'AFTER', value
:[[item:'2015-05-01']] ], ] ] ] ], findAttribute:[[item:'PartyUniqueName']]]
def findControl = [ ]
def acts = adf.webServices.SimplifiedAccountService.findAccount(findCriteria, findControl)
//Return all the Values for PartyUniqueName
def actlist = acts[0].Value.PartyUniqueName
//Return one specific row (first from index)
def actlist = acts[0].Value[0].PartyUniqueName

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 75


Handling Web Service Response Data
For Single Row Record Responses
Create Variable assigned to [ReturnObj].[FieldName]
Opty Example: def asn = rslt.OpportunityResource.AssignmentType

For Multiple Row Record Reponses, its ArrayList handling


To get all row values for PartyUniqueName field:
def actlist = acts[0].Value.PartyUniqueName
To get one row value for PartyUniqueName field:
def actlist = acts[0].Value[0].PartyUniqueName
To get all values/all rows
def actlist = acts[0].Value

Youll Also Need to Remove The [ and ]


def finalactList = substringAfter(substringBefore(actlist.toString(),']'),'[')

Alternative: toString() + RegEx


Copyright 2015, Oracle and/or its affiliates. All rights reserved. 76
Using Web Services To Get Data: Illustrative Examples
//Single Record - Return All Fields
def optyId = '300000002599040'
def rslt = adf.webServices.RichardsOpportunityService.getOpportunity(optyId)
return rslt

//Single Record - Return Just The Address Field


def optyId = '300000002599040'
def rslt = adf.webServices.RichardsOpportunityService.getOpportunity(optyId)
def adr = rslt.FormattedAddress
return adr

// Multi Record Response get all row values for PartyUniqueName


// def actlist = acts[0].Value.PartyUniqueName

//Multi Record Response Return first PartyName


// def actlist = acts[0].Value[0].PartyUniqueName

//Multi Record Response Return all fields for first record


def actlist = acts[0].Value

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 77


Hands-On Demo: Using Web Services To Get Data
Create a custom Formula Field
Set depends-on for something already on the page
Call a web service and return the result into the field (or one value)
Performance Considerations

More Advanced: Use Groovy to pass the web service different parameters
(or call a different services) depending on another Business Object field
value.

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 78


Using Web Services To Get Data: Hands-On Demo

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 79


Using Web Services To Get Data: Hands-On Demo
if(GroupSize > 100){
def optyId = '300000002599040'
def rslt = adf.webServices.RichardsOpportunityService.getOpportunity(optyId)
def adr = rslt.FormattedAddress
def asn = rslt.OpportunityResource.AssignmentType
return asn
}
else{
def findCriteria = [filter: [ group: [ [ item: [ [ attribute :'CreationDate', operator :'AFTER',
value :[[item:'2015-05-01']] ], ] ] ] ], findAttribute:[[item:'PartyUniqueName']]]
def findControl = [ ]
def acts = adf.webServices.SimplifiedAccountService.findAccount(findCriteria, findControl)
def actlist = acts[0].Value.PartyUniqueName
def finalactList = substringAfter(substringBefore(actlist.toString(),']'),'[')
return finalactList
}

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 80


JWT Tokens
Passing an encoded authorization key between systems
Uses: Hyperlinks, Menus, iFrames, Web Services

def userName = adf.context.getSecurityContext().getUserProfile().getUserID()


def partnerappurl=oracle.topologyManager.client.deployedInfo.DeployedInfoProvider.getEndPoint("PartnerApp")
def crmkey= (new oracle.apps.fnd.applcore.common.SecuredTokenBean().getTrustToken())
def opty = OptyId
def url = partnerappurl + "?" + "User=" + userName + "&CRMKEY="+crmkey + "&id=" + opty
return (url)

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 81


Data Architecture Summary
In this session we saw how to use:
Data From The Current Context Object
Data From A Related Collection (Children)
Data From A Related Object (Parents, Custom, DCLs)
Data From A View Object
Data From An Internal Web Service

Copyright 2015, Oracle and/or its affiliates. All rights reserved. 82


Get Stuck? = See My Team

READ: http://blogs.oracle.com/FADevRel
Over 200 expert technical articles, whitepapers, etc.

SEE: http://www.youtube.com/FADeveloperRelations
Over 150 demo how-to videos

ASK: http://bit.ly/CustForum
Over 500 questions answered

FOLLOW: @FADevRel

Copyright 2015, Oracle and/or its affiliates. All rights reserved.


Copyright 2015, Oracle and/or its affiliates. All rights reserved. 84

Вам также может понравиться