Skip to content

Commit

Permalink
feat(Export Command): #175 (#280)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Adds new command that will create a backup (undo) file
  • Loading branch information
ndickerson authored Jan 4, 2019
1 parent 12ed421 commit c288b3f
Show file tree
Hide file tree
Showing 43 changed files with 1,286 additions and 434 deletions.
1 change: 1 addition & 0 deletions dataloader.properties
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ loginUrl=https://rest.bullhornstaffing.com/rest-services/#
#clientCorporationCustomObjectInstance8ExistField=clientCorporation.externalID,text1
#clientCorporationCustomObjectInstance9ExistField=clientCorporation.externalID,text1
#clientCorporationCustomObjectInstance10ExistField=clientCorporation.externalID,text1
#clientCorporationCustomObjectInstance35ExistField=clientCorporation.externalID,text1
#jobOrderCustomObjectInstance1ExistField=jobOrder.externalID,text1
#jobOrderCustomObjectInstance2ExistField=jobOrder.externalID,text1
#jobOrderCustomObjectInstance3ExistField=jobOrder.externalID,text1
Expand Down
2 changes: 2 additions & 0 deletions examples/load/README.md → examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ These example CSV files reference several reference only entities that must exis

The maximal number of fields have been filled out in these examples, for as many entities as can be loaded. These files are as interconnected as possible, making use of as many association fields as possible. The `externalID` field is used if present, otherwise `customText1` is used to denote the external unique identifier for data that is being loaded. These are the same default exist fields that are used in section 3 of the `dataloader.properties` file, so that updating instead of inserting using these example files requires simply uncommenting the commented out exist fields in the properties file.

In order to ensure that all fields have been utilized, after updating the SDK-REST, run the command: `dataloader template <InputFile.csv>` for each example file in order to see which fields are missing or should be removed from the example file. For example: `dataloader template examples/load/Candidate.csv`. Not all fields can be added to the example file, but most can be.

### Integration Test

To perform a manual integration test (testing the integration between the DataLoader and the actual Rest API), do the following:
Expand Down
18 changes: 14 additions & 4 deletions src/main/java/com/bullhorn/dataloader/data/Result.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
*/
public class Result {

private Status status = Status.NOT_SET;
private Action action = Action.NOT_SET;
private Integer bullhornId = -1;
private Status status;
private Action action;
private Integer bullhornId;
private Integer bullhornParentId = -1;
private String failureText = "";
private String failureText;

public Result(Status status, Action action, Integer bullhornId, String failureText) {
this.status = status;
Expand Down Expand Up @@ -99,6 +99,15 @@ public static Result skip() {
return new Result(Status.SUCCESS, Action.SKIP, -1, "");
}

/**
* Convert convenience constructor
*
* @return The new Result object
*/
public static Result export(Integer bullhornId) {
return new Result(Status.SUCCESS, Action.EXPORT, bullhornId, "");
}

/**
* Failure convenience constructor
*
Expand Down Expand Up @@ -242,6 +251,7 @@ public enum Action {
DELETE,
CONVERT,
SKIP,
EXPORT,
FAILURE
}
}
9 changes: 5 additions & 4 deletions src/main/java/com/bullhorn/dataloader/enums/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
*/
public enum Command {

HELP("help"),
TEMPLATE("template"),
CONVERT_ATTACHMENTS("convertAttachments"),
LOAD("load"),
DELETE("delete"),
DELETE_ATTACHMENTS("deleteAttachments"),
EXPORT("export"),
HELP("help"),
LOAD("load"),
LOAD_ATTACHMENTS("loadAttachments"),
DELETE_ATTACHMENTS("deleteAttachments");
TEMPLATE("template");

private final String methodName;

Expand Down
114 changes: 88 additions & 26 deletions src/main/java/com/bullhorn/dataloader/rest/Field.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
import com.bullhornsdk.data.model.entity.embedded.Address;
import com.bullhornsdk.data.model.entity.embedded.OneToMany;
import com.google.common.collect.Lists;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

Expand All @@ -28,10 +30,10 @@ public class Field {
private final Cell cell;
private Boolean existField;
private final DateTimeFormatter dateTimeFormatter;
private Method getMethod = null;
private Method setMethod = null;
private Method getAssociationMethod = null;
private Method setAssociationMethod = null;
private Method getMethod;
private Method setMethod;
private Method getAssociationMethod;
private Method setAssociationMethod;

/**
* Constructor which takes the type of entity and the raw cell data.
Expand Down Expand Up @@ -100,10 +102,19 @@ public Boolean isToMany() {
* @return the name of the field (the direct field on this entity, or the direct field on the associated entity)
*/
public String getName() {
if (cell.isAssociation()) {
return cell.getAssociationFieldName();
}
return cell.getName();
return cell.isAssociation() ? cell.getAssociationFieldName() : cell.getName();
}

/**
* Returns the name of the field that is valid within the field parameter of a Get call.
*
* For direct fields, just the name of the field: firstName
* For compound fields, the name of the field
*
* @return the name of the field (the direct field on this entity, or the direct field on the associated entity)
*/
public String getFieldParameterName() {
return cell.isAssociation() ? cell.getAssociationBaseName() + "(" + cell.getAssociationFieldName() + ")" : cell.getName();
}

/**
Expand All @@ -112,21 +123,13 @@ public String getName() {
* @return this entity if direct, an associated entity if To-One or To-Many.
*/
public EntityInfo getFieldEntity() {
if (cell.isAssociation()) {
return AssociationUtil.getFieldEntity(entityInfo, cell);
}
return entityInfo;
return cell.isAssociation() ? AssociationUtil.getFieldEntity(entityInfo, cell) : entityInfo;
}

/**
* Returns the type of the field that this cell represents.
*
* This will be the simple data type. If an assoc public String getName() {
* if (cell.isAssociation()) {
* return cell.getAssociationFieldName();
* }
* return cell.getName();
* iation, the data type will be that of the association's field.
* This will be the simple data type. If an association, the data type will be that of the association's field.
*
* @return cannot be null, since that would throw an exception in the constructor.
*/
Expand Down Expand Up @@ -165,10 +168,58 @@ public List<String> split(String delimiter) {
return Lists.newArrayList(getStringValue().split(delimiter)).stream().distinct().collect(Collectors.toList());
}

/**
* Calls the appropriate get method on the given SDK-REST entity object in order to get the value from an entity.
*
* For Association fields, behaves differently when given a parent entity vs. the field entity.
* For Example, when dealing with the Note field: `candidates.id`:
* - When given a note entity, uses the given delimiter to combine all candidate ID values into one delimited string.
* - When given an individual Candidate entity, calls getId() method on the Candidate object.
*
* @param entity the entity to pull data from
* @param delimiter the character(s) to split on
* @return the string value of this field on the given entity
*/
public String getStringValueFromEntity(Object entity, String delimiter) throws InvocationTargetException, IllegalAccessException {
if (cell.isAssociation() && entityInfo.getEntityClass().equals(entity.getClass())) {
if (isToMany()) {
List<String> values = new ArrayList<>();
OneToMany toManyAssociation = (OneToMany) getAssociationMethod.invoke(entity);
if (toManyAssociation != null) {
for (Object association : toManyAssociation.getData()) {
Object value = getMethod.invoke(association);
if (value != null) {
String stringValue = String.valueOf(value);
values.add(stringValue);
}
}
}
return String.join(delimiter, values);
} else {
Object toOneAssociation = getAssociationMethod.invoke(entity);
if (toOneAssociation == null) {
return "";
}
Object value = getMethod.invoke(toOneAssociation);
return value != null ? String.valueOf(value) : "";
}
}

Object value = getMethod.invoke(entity);
if (value == null) {
return "";
}
if (DateTime.class.equals(value.getClass())) {
DateTime dateTime = (DateTime) value;
return dateTimeFormatter.print(dateTime);
}
return String.valueOf(value);
}

/**
* Calls the appropriate set method on the given SDK-REST entity object in order to send the entity in a REST call.
*
* This only applies to direct or compound (address) fields, that have a simple value type.
* This only applies to direct or compound (address) fields that have a simple value type.
*
* @param entity the entity object to populate
*/
Expand All @@ -195,9 +246,7 @@ public void populateFieldOnEntity(Object entity) throws ParseException, Invocati
public void populateAssociationOnEntity(BullhornEntity entity, BullhornEntity associatedEntity) throws
ParseException, InvocationTargetException, IllegalAccessException {
setMethod.invoke(associatedEntity, getValue());
if (isToOne()) {
setAssociationMethod.invoke(entity, associatedEntity);
} else {
if (isToMany()) {
OneToMany<BullhornEntity> oneToMany = (OneToMany<BullhornEntity>) getAssociationMethod.invoke(entity);
if (oneToMany == null) {
oneToMany = new OneToMany<>();
Expand All @@ -206,15 +255,28 @@ public void populateAssociationOnEntity(BullhornEntity entity, BullhornEntity as
associations.add(associatedEntity);
oneToMany.setData(associations);
setAssociationMethod.invoke(entity, oneToMany);
} else {
setAssociationMethod.invoke(entity, associatedEntity);
}
}

/**
* Calls the appropriate get method on the given SDK-REST entity object in order to get the value from an entity.
* Returns the oneToMany object for a To-Many association.
*
* @param entity the entity object to get the association value from.
*/
@SuppressWarnings("unchecked")
public OneToMany getOneToManyFromEntity(BullhornEntity entity) throws InvocationTargetException, IllegalAccessException {
return (OneToMany) getAssociationMethod.invoke(entity);
}

/**
* Sets a To-Many field of a given entity to the given object.
*
* @param entity the entity object to get the value of the field from
* @param entity the entity object to populate
* @param oneToMany the OneToMany object for this To-Many field
*/
public Object getValueFromEntity(BullhornEntity entity) throws InvocationTargetException, IllegalAccessException {
return getMethod.invoke(entity);
public void populateOneToManyOnEntity(BullhornEntity entity, OneToMany oneToMany) throws InvocationTargetException, IllegalAccessException {
setAssociationMethod.invoke(entity, oneToMany);
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/bullhorn/dataloader/rest/Record.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -73,4 +74,13 @@ public List<Field> getToManyFields() {
return fields.stream().filter(field -> field.isToMany() && !field.getStringValue().isEmpty()).collect(Collectors.toList());
}
}

/**
* Returns all fields in a format that can be passed as the fields parameter of a Get call.
*
* @return the set of fields that will pull from Rest
*/
public Set<String> getFieldsParameter() {
return fields.stream().map(Field::getFieldParameterName).collect(Collectors.toSet());
}
}
19 changes: 12 additions & 7 deletions src/main/java/com/bullhorn/dataloader/rest/RestApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
* Encapsulation of the standard SDK-REST BullhornData class for interacting with Bullhorn's REST API Provides an extra layer of functionality needed
Expand Down Expand Up @@ -78,29 +79,33 @@ public <T extends SearchEntity> List<T> searchForList(Class<T> type,
String query,
Set<String> fieldSet,
SearchParams params) {
printUtil.log(Level.DEBUG, "Find(" + type.getSimpleName() + " Search): " + query);
Boolean isSupportedEntity = type != JobOrder.class && type != Lead.class && type != Opportunity.class;
Set<String> correctedFieldSet = FindUtil.getCorrectedFieldSet(fieldSet);
printUtil.log(Level.DEBUG, "Find(" + type.getSimpleName() + " Search): " + query
+ ", fields: " + correctedFieldSet.stream().sorted().collect(Collectors.toList()));
boolean isSupportedEntity = type != JobOrder.class && type != Lead.class && type != Opportunity.class;
String externalId = FindUtil.getExternalIdValue(query);
if (isSupportedEntity && !externalId.isEmpty()) {
SearchResult<T> searchResult = restApiExtension.getByExternalId(this, type, externalId, fieldSet);
SearchResult<T> searchResult = restApiExtension.getByExternalId(this, type, externalId, correctedFieldSet);
if (searchResult.getSuccess()) {
return searchResult.getList();
}
}
List<T> list = new ArrayList<>();
params.setCount(MAX_RECORDS_TO_RETURN_IN_ONE_PULL);
recursiveSearchPull(list, type, query, fieldSet, params);
recursiveSearchPull(list, type, query, correctedFieldSet, params);
return list;
}

public <T extends QueryEntity> List<T> queryForList(Class<T> type,
String where,
Set<String> fieldSet,
QueryParams params) {
printUtil.log(Level.DEBUG, "Find(" + type.getSimpleName() + " Query): " + where);
Set<String> correctedFieldSet = FindUtil.getCorrectedFieldSet(fieldSet);
printUtil.log(Level.DEBUG, "Find(" + type.getSimpleName() + " Query): " + where
+ ", fields: " + correctedFieldSet.stream().sorted().collect(Collectors.toList()));
List<T> list = new ArrayList<>();
params.setCount(MAX_RECORDS_TO_RETURN_IN_ONE_PULL);
recursiveQueryPull(list, type, where, fieldSet, params);
recursiveQueryPull(list, type, where, correctedFieldSet, params);
return list;
}
// endregion
Expand Down Expand Up @@ -140,7 +145,7 @@ public <T extends AssociationEntity, E extends BullhornEntity> List<E> getAllAss
Class<T> type, Set<Integer> entityIds, AssociationField<T, E> associationName, Set<String> fieldSet,
AssociationParams params) {
printUtil.log(Level.DEBUG, "FindAssociations(" + type.getSimpleName() + "): #" + entityIds + " - "
+ associationName.getAssociationFieldName());
+ associationName.getAssociationFieldName() + ", fields: " + fieldSet.stream().sorted().collect(Collectors.toList()));
ListWrapper<E> listWrapper = bullhornData.getAllAssociations(type, entityIds, associationName, fieldSet, params);
return listWrapper == null ? Collections.emptyList() : listWrapper.getData();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import com.bullhorn.dataloader.util.ValidationUtil;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -39,7 +38,7 @@ public AbstractService(PrintUtil printUtil,
RestSession restSession,
ProcessRunner processRunner,
InputStream inputStream,
Timer timer) throws IOException {
Timer timer) {
this.printUtil = printUtil;
this.propertyFileUtil = propertyFileUtil;
this.validationUtil = validationUtil;
Expand All @@ -64,7 +63,7 @@ protected Boolean promptUserForMultipleFiles(String filePath, SortedMap<EntityIn
|| (!entityToFileListMap.isEmpty() && entityToFileListMap.get(entityToFileListMap.firstKey()).size() > 1)) {
printUtil.printAndLog("Ready to process the following CSV files from the " + filePath + " directory in the following order:");

Integer count = 1;
int count = 1;
for (Map.Entry<EntityInfo, List<String>> entityFileEntry : entityToFileListMap.entrySet()) {
String entityName = entityFileEntry.getKey().getEntityName();
for (String fileName : entityFileEntry.getValue()) {
Expand All @@ -75,7 +74,7 @@ protected Boolean promptUserForMultipleFiles(String filePath, SortedMap<EntityIn

printUtil.print("Do you want to continue? [Y/N]");
Scanner scanner = new Scanner(inputStream);
Boolean yesOrNoResponse = false;
boolean yesOrNoResponse = false;
while (!yesOrNoResponse) {
String input = scanner.nextLine();
if (input.startsWith("y") || input.startsWith("Y")) {
Expand Down
Loading

0 comments on commit c288b3f

Please # to comment.