diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..e21d04d --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,37 @@ +name: Build and test for the pull request + +on: + pull_request: + types: [opened, synchronize] + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[ci skip]')" + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + server-id: ossrh + server-username: OSSRH_JIRA_USERNAME + server-password: OSSRH_JIRA_PASSWORD + gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }} + gpg-passphrase: OSSRH_GPG_SECRET_KEY_PASSWORD + - uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Build with Maven + run: mvn -B package + - name: Deploy SNAPSHOT version + run: mvn -B -DskipTests deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OSSRH_JIRA_USERNAME: ${{ secrets.OSSRH_JIRA_USERNAME }} + OSSRH_JIRA_PASSWORD: ${{ secrets.OSSRH_JIRA_PASSWORD }} + OSSRH_GPG_SECRET_KEY_PASSWORD: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ed1f3df --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,64 @@ +name: Release package to the Maven Central Repository + +on: + push: + branches: + - main + +jobs: + build: + name: Build and release + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[ci skip]')" + steps: + - uses: actions/checkout@v2 + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: 11 + server-id: ossrh + server-username: OSSRH_JIRA_USERNAME + server-password: OSSRH_JIRA_PASSWORD + gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }} + gpg-passphrase: OSSRH_GPG_SECRET_KEY_PASSWORD + - name: Cache for maven + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Setup node + uses: actions/setup-node@v1 + with: + node-version: '10.x' + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Cache for yarn + uses: actions/cache@v2 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-maven-semantic-release-v4.5.0 + restore-keys: | + ${{ runner.os }}-yarn- + - name: Setup semantic-release + run: | + yarn global add @conveyal/maven-semantic-release@v4.5.0 semantic-release@15 + echo "$(yarn global bin)" >> $GITHUB_PATH + - name: Test + run: mvn -B test + - name: Release + # maven-semantic-release requires "maven-settings.xml" in the workspace directory + run: | + mv ~/.m2/settings.xml maven-settings.xml + semantic-release --branch main --prepare @conveyal/maven-semantic-release \ + --publish @semantic-release/github,@conveyal/maven-semantic-release \ + --verify-conditions @semantic-release/github,@conveyal/maven-semantic-release \ + --verify-release @conveyal/maven-semantic-release\ + --use-conveyal-workflow + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OSSRH_JIRA_USERNAME: ${{ secrets.OSSRH_JIRA_USERNAME }} + OSSRH_JIRA_PASSWORD: ${{ secrets.OSSRH_JIRA_PASSWORD }} + OSSRH_GPG_SECRET_KEY_PASSWORD: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2ebb29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target +*.jar +.idea +.DS_Store + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f943cfd --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# SmartHR Connector + +## Description + +Connector for [SmartHR](https://smarthr.jp/). + +## Capabilities and Features + +- Schema: YES +- Provisioning: YES +- Live Synchronization: No +- Password: YES +- Activation: YES +- Script execution: No + +## Build + +Install JDK 11+ and [maven3](https://maven.apache.org/download.cgi) then build: + +``` +mvn install +``` + +After successful the build, you can find `connector-smarthr-*.jar` in `target` directory. + +## License + +Licensed under the [Apache License 2.0](/LICENSE). diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..87ab7ea --- /dev/null +++ b/pom.xml @@ -0,0 +1,268 @@ + + 4.0.0 + + + connector-parent + com.evolveum.polygon + 1.5.0.0 + + + + jp.openstandia.connector + connector-smarthr + 0.0.2-SNAPSHOT + jar + + SmartHR Connector + + + SmartHR Connector. + + https://github.com/openstandia/connector-smarthr + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + repo + + + + + + wadahiro + Hiroyuki Wada + h2-wada@nri.co.jp + Nomura Research Institute, Ltd. + +9 + + + + + scm:git:https://github.com/openstandia/connector-smarthr.git + scm:git:https://github.com/openstandia/connector-smarthr.git + https://github.com/openstandia/connector-smarthr + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + jp.openstandia.connector.smarthr + SmartHRConnector + + + + + central + Maven Central + https://repo1.maven.org/maven2 + + + evolveum-nexus-releases + Internal Releases + https://nexus.evolveum.com/nexus/content/repositories/releases/ + + + evolveum-nexus-snapshots + Internal Releases + https://nexus.evolveum.com/nexus/content/repositories/snapshots/ + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + org.apache.maven.surefire + surefire-junit-platform + 2.22.2 + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + ossrh + https://oss.sonatype.org/ + true + 20 + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.0 + + + attach-sources + verify + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + ${project.source.version} + ${java.home}/bin/javadoc + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + --pinentry-mode + loopback + + + + + sign-artifacts + verify + + sign + + + + + + + + + + com.squareup.okhttp3 + okhttp + 4.9.0 + + + com.squareup.okhttp3 + okhttp-urlconnection + 4.9.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.12.0 + + + + org.junit.jupiter + junit-jupiter + 5.6.0 + test + + + + + + midpoint + + + com.evolveum.midpoint.gui + admin-gui + 4.4.2 + jar + classes + provided + + + org.testng + testng + 6.8 + test + + + org.yaml + snakeyaml + + + + + net.tirasa.connid + connector-framework-contract + ${connId.version} + test + + + net.tirasa.connid + connector-test-common + + + org.codehaus.groovy + groovy + + + org.codehaus.groovy + groovy-templates + + + org.codehaus.groovy + groovy-bsf + + + org.codehaus.groovy + groovy-jsr223 + + + org.codehaus.groovy + groovy-datetime + + + org.codehaus.groovy + groovy-dateutil + + + org.codehaus.groovy + groovy-sql + + + + + + + diff --git a/src/main/assembly/connector.xml b/src/main/assembly/connector.xml new file mode 100644 index 0000000..a2e6d0b --- /dev/null +++ b/src/main/assembly/connector.xml @@ -0,0 +1,43 @@ + + + + bundle + + + jar + + + false + + + + ${project.basedir} + + + LICENSE + README.md + + + + ${project.build.directory}/classes + + + + + + + lib + true + false + + net.tirasa.connid:connector-framework + net.tirasa.connid:connector-framework-internal + jp.openstandia.connector:connector-smarthr + + + + + \ No newline at end of file diff --git a/src/main/java/jp/openstandia/connector/smarthr/SchemaDefinition.java b/src/main/java/jp/openstandia/connector/smarthr/SchemaDefinition.java new file mode 100644 index 0000000..61638a2 --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SchemaDefinition.java @@ -0,0 +1,720 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; +import org.identityconnectors.framework.common.objects.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static jp.openstandia.connector.smarthr.SmartHRUtils.shouldReturn; + +public class SchemaDefinition { + + public static Builder newBuilder(ObjectClass objectClass) { + Builder schemaBuilder = new Builder(objectClass); + return schemaBuilder; + } + + public static class Builder { + private final ObjectClass objectClass; + private final List attributes = new ArrayList<>(); + + public Builder(ObjectClass objectClass) { + this.objectClass = objectClass; + } + + public void addUid(String name, + Types typeClass, + Class createClass, + Class updateClass, + Class readClass, + + BiConsumer create, + BiConsumer update, + Function read, + + String fetchField, + + SchemaOption... options + ) { + AttributeMapper attr = new AttributeMapper(Uid.NAME, name, typeClass, create, update, read, fetchField, options); + this.attributes.add(attr); + } + + public void addUid(String name, + Types typeClass, + Class createOrUpdateClass, + Class readClass, + + BiConsumer createOrUpdate, + Function read, + + String fetchField, + + SchemaOption... options + ) { + AttributeMapper attr = new AttributeMapper(Uid.NAME, name, typeClass, createOrUpdate, createOrUpdate, read, fetchField, options); + this.attributes.add(attr); + } + + public void addName(String name, + Types typeClass, + Class createClass, + Class updateClass, + Class readClass, + + BiConsumer create, + BiConsumer update, + Function read, + + String fetchField, + + SchemaOption... options + ) { + AttributeMapper attr = new AttributeMapper(Name.NAME, name, typeClass, create, update, read, fetchField, options); + this.attributes.add(attr); + } + + public void addName(String name, + Types typeClass, + Class createOrUpdateClass, + Class readClass, + + BiConsumer createOrUpdate, + Function read, + + String fetchField, + + SchemaOption... options + ) { + AttributeMapper attr = new AttributeMapper(Name.NAME, name, typeClass, createOrUpdate, createOrUpdate, read, fetchField, options); + this.attributes.add(attr); + } + + public void add(String name, + Types typeClass, + Class createClass, + Class updateClass, + Class readClass, + + BiConsumer create, + BiConsumer update, + Function read, + + String fetchField, + + SchemaOption... options + ) { + AttributeMapper attr = new AttributeMapper(name, typeClass, create, update, read, fetchField, options); + this.attributes.add(attr); + } + + public void add(String name, + Types typeClass, + Class createOrUpdateClass, + Class readClass, + + BiConsumer createOrUpdate, + Function read, + + String fetchField, + + SchemaOption... options + ) { + AttributeMapper attr = new AttributeMapper(name, typeClass, createOrUpdate, createOrUpdate, read, fetchField, options); + this.attributes.add(attr); + } + + public void addAsMultiple(String name, + Types typeClass, + Class createClass, + Class updateClass, + Class readClass, + + BiConsumer, C> create, + BiConsumer, U> updateAdd, + BiConsumer, U> updateRemove, + Function> read, + + String fetchField, + + SchemaOption... options + ) { + AttributeMapper attr = new AttributeMapper(name, typeClass, create, updateAdd, updateRemove, read, fetchField, options); + this.attributes.add(attr); + } + + public SchemaDefinition build() { + SchemaDefinition schemaDefinition = new SchemaDefinition(objectClass, buildSchemaInfo(), buildAttributeMap()); + return schemaDefinition; + } + + private ObjectClassInfo buildSchemaInfo() { + List list = attributes.stream() + .map(attr -> { + AttributeInfoBuilder define = AttributeInfoBuilder.define(attr.connectorName); + + define.setType(attr.type.typeClass); + define.setMultiValued(attr.isMultiple); + define.setNativeName(attr.name); + + if (attr.type == Types.UUID) { + define.setSubtype(AttributeInfo.Subtypes.STRING_UUID); + + } else if (attr.type == Types.STRING_CASE_IGNORE) { + define.setSubtype(AttributeInfo.Subtypes.STRING_CASE_IGNORE); + + } else if (attr.type == Types.STRING_URI) { + define.setSubtype(AttributeInfo.Subtypes.STRING_URI); + + } else if (attr.type == Types.STRING_LDAP_DN) { + define.setSubtype(AttributeInfo.Subtypes.STRING_LDAP_DN); + + } else if (attr.type == Types.XML) { + define.setSubtype(AttributeInfo.Subtypes.STRING_XML); + + } else if (attr.type == Types.JSON) { + define.setSubtype(AttributeInfo.Subtypes.STRING_JSON); + } + + for (SchemaOption option : attr.options) { + switch (option) { + case REQUIRED: { + define.setRequired(true); + break; + } + case NOT_CREATABLE: { + define.setCreateable(false); + break; + } + case NOT_UPDATABLE: { + define.setUpdateable(false); + break; + } + case NOT_READABLE: { + define.setReadable(false); + break; + } + case NOT_RETURN_BY_DEFAULT: { + define.setReturnedByDefault(false); + break; + } + } + } + + return define.build(); + }) + .collect(Collectors.toList()); + + ObjectClassInfoBuilder builder = new ObjectClassInfoBuilder(); + builder.setType(objectClass.getObjectClassValue()); + builder.addAllAttributeInfo(list); + + return builder.build(); + } + + private Map buildAttributeMap() { + Map map = attributes.stream() + // Use connectorName for the key (to lookup by special name like __UID__ + .collect(Collectors.toMap(a -> a.connectorName, a -> a)); + return map; + } + } + + private final ObjectClass objectClass; + private final ObjectClassInfo objectClassInfo; + private final Map attributeMap; + // Key: attribute name (for connector. e.g. __NAME__) + // Value: field name for resource fetching + private final Map returnedByDefaultAttributesSet; + + public SchemaDefinition(ObjectClass objectClass, ObjectClassInfo objectClassInfo, Map attributeMap) { + this.objectClass = objectClass; + this.objectClassInfo = objectClassInfo; + this.attributeMap = attributeMap; + this.returnedByDefaultAttributesSet = getObjectClassInfo().getAttributeInfo().stream() + .filter(i -> i.isReturnedByDefault()) + .map(i -> i.getName()) + .collect(Collectors.toMap(n -> n, n -> attributeMap.get(n).fetchField)); + } + + public ObjectClassInfo getObjectClassInfo() { + return objectClassInfo; + } + + public Map getReturnedByDefaultAttributesSet() { + return returnedByDefaultAttributesSet; + } + + public String getFetchField(String name) { + AttributeMapper attributeMapper = attributeMap.get(name); + if (attributeMapper != null) { + return attributeMapper.fetchField; + } + return null; + } + + public T apply(Set attrs, T dest) { + for (Attribute attr : attrs) { + AttributeMapper attributeMapper = attributeMap.get(attr.getName()); + if (attributeMapper == null) { + throw new InvalidAttributeValueException("Invalid attribute: " + attr.getName()); + } + + attributeMapper.apply(attr, dest); + } + return dest; + } + + public boolean applyDelta(Set deltas, U dest) { + boolean changed = false; + for (AttributeDelta delta : deltas) { + AttributeMapper attributeMapper = attributeMap.get(delta.getName()); + if (attributeMapper == null) { + throw new InvalidAttributeValueException("Invalid attribute: " + delta.getName()); + } + + attributeMapper.apply(delta, dest); + changed = true; + } + return changed; + } + + public ConnectorObjectBuilder toConnectorObjectBuilder(R source, Set attributesToGet, boolean allowPartialAttributeValues) { + final ConnectorObjectBuilder builder = new ConnectorObjectBuilder() + .setObjectClass(objectClass); + + AttributeMapper uid = attributeMap.get(Uid.NAME); + builder.addAttribute(uid.apply(source)); + + // Need to set __NAME__ because it throws IllegalArgumentException + AttributeMapper name = attributeMap.get(Name.NAME); + builder.addAttribute(name.apply(source)); + + for (Map.Entry entry : attributeMap.entrySet()) { + if (shouldReturn(attributesToGet, entry.getKey(), returnedByDefaultAttributesSet.containsKey(entry.getKey()))) { + Attribute value = entry.getValue().apply(source); + if (value != null) { + builder.addAttribute(value); + } + } + } + + return builder; + } + + public String getType() { + return objectClassInfo.getType(); + } + + public static class Types { + public static final Types STRING = new Types(String.class); + public static final Types STRING_CASE_IGNORE = new Types(String.class); + public static final Types STRING_URI = new Types(String.class); + public static final Types STRING_LDAP_DN = new Types(String.class); + public static final Types XML = new Types(String.class); + public static final Types JSON = new Types(String.class); + public static final Types UUID = new Types(String.class); + public static final Types INTEGER = new Types(Integer.class); + public static final Types LONG = new Types(Long.class); + public static final Types FLOAT = new Types(Float.class); + public static final Types DOUBLE = new Types(Double.class); + public static final Types BOOLEAN = new Types(Boolean.class); + public static final Types BIG_DECIMAL = new Types(BigDecimal.class); + public static final Types DATE_STRING = new Types(ZonedDateTime.class); + public static final Types DATETIME_STRING = new Types(ZonedDateTime.class); + public static final Types DATE = new Types(ZonedDateTime.class); + public static final Types DATETIME = new Types(ZonedDateTime.class); + + private final Class typeClass; + + private Types(Class typeClass) { + this.typeClass = typeClass; + } + } + + public static SchemaOption[] newOptions(SchemaOption... options) { + return options; + } + + static enum SchemaOption { + REQUIRED, + NOT_CREATABLE, + NOT_UPDATABLE, + NOT_READABLE, + NOT_RETURN_BY_DEFAULT, + } + + static class AttributeMapper { + private final String connectorName; + private final String name; + private final Types type; + boolean isMultiple; + + private final BiConsumer create; + private final BiConsumer replace; + private final BiConsumer, U> add; + private final BiConsumer, U> remove; + private final Function read; + + private final String fetchField; + + private final SchemaOption[] options; + + private DateTimeFormatter dateFormat; + private DateTimeFormatter dateTimeFormat; + + private static final DateTimeFormatter DEFAULT_DATE_FORMAT = DateTimeFormatter.ISO_LOCAL_DATE; + private static final DateTimeFormatter DEFAULT_DATE_TIME_FORMAT = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + + public AttributeMapper(String connectorName, String name, Types typeClass, + BiConsumer create, + BiConsumer replace, + Function read, + String fetchField, + SchemaOption... options + ) { + this(connectorName, name, typeClass, create, replace, null, null, read, fetchField, false, options); + } + + public AttributeMapper(String name, Types typeClass, + BiConsumer create, + BiConsumer replace, + Function read, + String fetchField, + SchemaOption... options + ) { + this(name, name, typeClass, create, replace, null, null, read, fetchField, false, options); + } + + public AttributeMapper(String name, Types typeClass, + BiConsumer create, + BiConsumer, U> add, + BiConsumer, U> remove, + Function read, + String fetchField, + SchemaOption... options + ) { + this(name, name, typeClass, create, null, add, remove, read, fetchField, true, options); + } + + public AttributeMapper(String connectorName, String name, Types typeClass, + BiConsumer create, + BiConsumer replace, + BiConsumer, U> add, + BiConsumer, U> remove, + Function read, + String fetchField, + boolean isMultiple, + SchemaOption... options + ) { + this.connectorName = connectorName; + this.name = name; + this.type = typeClass; + this.create = create; + this.replace = replace; + this.add = add; + this.remove = remove; + this.read = read; + this.fetchField = fetchField != null ? fetchField : name; + this.options = options; + this.isMultiple = isMultiple; + } + + public boolean isStringType() { + return type == Types.STRING || type == Types.STRING_URI || type == Types.STRING_LDAP_DN || + type == Types.STRING_LDAP_DN || type == Types.STRING_CASE_IGNORE || type == Types.XML || + type == Types.JSON || type == Types.UUID; + } + + public AttributeMapper dateFormat(DateTimeFormatter dateFormat) { + this.dateFormat = dateFormat; + return this; + } + + public AttributeMapper datetimeFormat(DateTimeFormatter datetimeFormat) { + this.dateTimeFormat = datetimeFormat; + return this; + } + + private String formatDate(ZonedDateTime zonedDateTime) { + if (this.dateFormat == null) { + return zonedDateTime.format(DEFAULT_DATE_FORMAT); + } + return zonedDateTime.format(this.dateFormat); + } + + private String formatDateTime(ZonedDateTime zonedDateTime) { + if (this.dateTimeFormat == null) { + return zonedDateTime.format(DEFAULT_DATE_TIME_FORMAT); + } + return zonedDateTime.format(this.dateFormat); + } + + private ZonedDateTime toDate(String dateString) { + LocalDate date; + if (this.dateFormat == null) { + date = LocalDate.parse(dateString, DEFAULT_DATE_FORMAT); + } else { + date = LocalDate.parse(dateString, this.dateFormat); + } + return date.atStartOfDay(ZoneId.systemDefault()); + } + + private ZonedDateTime toDateTime(String dateTimeString) { + ZonedDateTime dateTime; + if (this.dateTimeFormat == null) { + dateTime = ZonedDateTime.parse(dateTimeString, DEFAULT_DATE_TIME_FORMAT); + } else { + dateTime = ZonedDateTime.parse(dateTimeString, this.dateTimeFormat); + } + return dateTime; + } + + public void apply(Attribute source, C dest) { + if (create == null) { + return; + } + + if (isMultiple) { + if (type == Types.DATE_STRING) { + List values = source.getValue().stream() + .map(v -> (ZonedDateTime) v) + .map(v -> (T) formatDate(v)) + .collect(Collectors.toList()); + create.accept((T) values, dest); + + } else if (type == Types.DATETIME_STRING) { + List values = source.getValue().stream() + .map(v -> (ZonedDateTime) v) + .map(v -> (T) formatDateTime(v)) + .collect(Collectors.toList()); + create.accept((T) values, dest); + + } else { + List values = source.getValue().stream().map(v -> (T) v).collect(Collectors.toList()); + create.accept((T) values, dest); + } + + } else { + if (isStringType()) { + String value = AttributeUtil.getAsStringValue(source); + create.accept((T) value, dest); + + } else if (type == Types.INTEGER) { + Integer value = AttributeUtil.getIntegerValue(source); + create.accept((T) value, dest); + + } else if (type == Types.LONG) { + Long value = AttributeUtil.getLongValue(source); + create.accept((T) value, dest); + + } else if (type == Types.FLOAT) { + Float value = AttributeUtil.getFloatValue(source); + create.accept((T) value, dest); + + } else if (type == Types.DOUBLE) { + Double value = AttributeUtil.getDoubleValue(source); + create.accept((T) value, dest); + + } else if (type == Types.BOOLEAN) { + Boolean value = AttributeUtil.getBooleanValue(source); + create.accept((T) value, dest); + + } else if (type == Types.BIG_DECIMAL) { + BigDecimal value = AttributeUtil.getBigDecimalValue(source); + create.accept((T) value, dest); + + } else if (type == Types.DATE || type == Types.DATETIME) { + ZonedDateTime date = (ZonedDateTime) AttributeUtil.getSingleValue(source); + String formatted = formatDate(date); + create.accept((T) formatted, dest); + + } else if (type == Types.DATE_STRING) { + ZonedDateTime date = (ZonedDateTime) AttributeUtil.getSingleValue(source); + String formatted = formatDate(date); + create.accept((T) formatted, dest); + + } else if (type == Types.DATETIME_STRING) { + ZonedDateTime date = (ZonedDateTime) AttributeUtil.getSingleValue(source); + String formatted = formatDateTime(date); + create.accept((T) formatted, dest); + } + } + } + + public void apply(AttributeDelta source, U dest) { + if (isMultiple) { + if (add == null || remove == null) { + return; + } + + if (type == Types.DATE_STRING) { + List valuesToAdd = safeStream(source.getValuesToAdd()) + .map(v -> (ZonedDateTime) v) + .map(v -> (T) formatDate(v)) + .collect(Collectors.toList()); + List valuesToRemove = safeStream(source.getValuesToRemove()) + .map(v -> (ZonedDateTime) v) + .map(v -> (T) formatDate(v)) + .collect(Collectors.toList()); + + if (!valuesToAdd.isEmpty()) { + add.accept(valuesToAdd, dest); + } + if (!valuesToRemove.isEmpty()) { + remove.accept(valuesToRemove, dest); + } + + } else if (type == Types.DATETIME_STRING) { + List valuesToAdd = safeStream(source.getValuesToAdd()) + .map(v -> (ZonedDateTime) v) + .map(v -> (T) formatDateTime(v)) + .collect(Collectors.toList()); + List valuesToRemove = safeStream(source.getValuesToRemove()) + .map(v -> (ZonedDateTime) v) + .map(v -> (T) formatDateTime(v)) + .collect(Collectors.toList()); + + if (!valuesToAdd.isEmpty()) { + add.accept(valuesToAdd, dest); + } + if (!valuesToRemove.isEmpty()) { + remove.accept(valuesToRemove, dest); + } + + } else { + List valuesToAdd = safeStream(source.getValuesToAdd()).map(v -> (T) v).collect(Collectors.toList()); + List valuesToRemove = safeStream(source.getValuesToRemove()).map(v -> (T) v).collect(Collectors.toList()); + + if (!valuesToAdd.isEmpty()) { + add.accept(valuesToAdd, dest); + } + if (!valuesToRemove.isEmpty()) { + remove.accept(valuesToRemove, dest); + } + } + + } else { + if (replace == null) { + return; + } + + if (isStringType()) { + String value = AttributeDeltaUtil.getAsStringValue(source); + replace.accept((T) value, dest); + + } else if (type == Types.INTEGER) { + Integer value = AttributeDeltaUtil.getIntegerValue(source); + replace.accept((T) value, dest); + + } else if (type == Types.LONG) { + Long value = AttributeDeltaUtil.getLongValue(source); + replace.accept((T) value, dest); + + } else if (type == Types.FLOAT) { + Float value = AttributeDeltaUtil.getFloatValue(source); + replace.accept((T) value, dest); + + } else if (type == Types.DOUBLE) { + Double value = AttributeDeltaUtil.getDoubleValue(source); + replace.accept((T) value, dest); + + } else if (type == Types.BOOLEAN) { + Boolean value = AttributeDeltaUtil.getBooleanValue(source); + replace.accept((T) value, dest); + + } else if (type == Types.BIG_DECIMAL) { + BigDecimal value = AttributeDeltaUtil.getBigDecimalValue(source); + replace.accept((T) value, dest); + + } else if (type == Types.DATE || type == Types.DATETIME) { + ZonedDateTime date = (ZonedDateTime) AttributeDeltaUtil.getSingleValue(source); + replace.accept((T) date, dest); + + } else if (type == Types.DATE_STRING) { + ZonedDateTime date = (ZonedDateTime) AttributeDeltaUtil.getSingleValue(source); + String formatted = formatDate(date); + replace.accept((T) formatted, dest); + + } else if (type == Types.DATETIME_STRING) { + ZonedDateTime date = (ZonedDateTime) AttributeDeltaUtil.getSingleValue(source); + String formatted = formatDateTime(date); + replace.accept((T) formatted, dest); + } + } + } + + public Attribute apply(R source) { + if (read == null) { + return null; + } + + Object value = read.apply(source); + if (value == null) { + return null; + } + + if (isMultiple) { + if (type == Types.DATE_STRING) { + List values = ((List) value).stream() + .map(v -> (String) v) + .map(v -> toDate(v)) + .collect(Collectors.toList()); + return AttributeBuilder.build(connectorName, values); + + } else if (type == Types.DATETIME_STRING) { + List values = ((List) value).stream() + .map(v -> (String) v) + .map(v -> toDateTime(v)) + .collect(Collectors.toList()); + return AttributeBuilder.build(connectorName, values); + + } else { + return AttributeBuilder.build(connectorName, (List) value); + } + + } else { + if (type == Types.DATE_STRING) { + ZonedDateTime date = toDate(value.toString()); + return AttributeBuilder.build(connectorName, date); + + } else if (type == Types.DATETIME_STRING) { + ZonedDateTime dateTime = toDateTime(value.toString()); + return AttributeBuilder.build(connectorName, dateTime); + } + return AttributeBuilder.build(connectorName, value); + } + } + + private Stream safeStream(List list) { + if (list == null) { + return Collections.emptyList().stream(); + } + return list.stream(); + } + } +} \ No newline at end of file diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRBizEstablishmentHandler.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRBizEstablishmentHandler.java new file mode 100644 index 0000000..bf1d026 --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRBizEstablishmentHandler.java @@ -0,0 +1,190 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.UnknownUidException; +import org.identityconnectors.framework.common.objects.*; + +import java.util.Set; + +import static jp.openstandia.connector.smarthr.SchemaDefinition.SchemaOption.*; + +public class SmartHRBizEstablishmentHandler implements SmartHRObjectHandler { + + public static final ObjectClass BIZ_ESTABLISHMENT_OBJECT_CLASS = new ObjectClass("biz_establishment"); + + private static final Log LOGGER = Log.getLog(SmartHRBizEstablishmentHandler.class); + + private final SmartHRConfiguration configuration; + private final SmartHRClient client; + private final SchemaDefinition schema; + + public SmartHRBizEstablishmentHandler(SmartHRConfiguration configuration, SmartHRClient client, SchemaDefinition schema) { + this.configuration = configuration; + this.client = client; + this.schema = schema; + } + + public static SchemaDefinition.Builder createSchema() { + SchemaDefinition.Builder sb = SchemaDefinition.newBuilder(BIZ_ESTABLISHMENT_OBJECT_CLASS); + + // __UID__ + // The id for the biz_establishment. Must be unique within the SmartHR tenant and unchangeable. + // Also, it's UUID (case-insensitive). + // We can't use "id" for the schema because of conflict in midpoint. + sb.addUid("biz_establishment_id", + SchemaDefinition.Types.UUID, + SmartHRClient.BizEstablishment.class, + SmartHRClient.BizEstablishment.class, + null, + (source) -> source.id, + "id", + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE + ); + + // __NAME__ + sb.addName("name", + SchemaDefinition.Types.STRING, + SmartHRClient.BizEstablishment.class, + SmartHRClient.BizEstablishment.class, + (source, dest) -> dest.name = source, + (source) -> source.name, + null, + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE + ); + + // 社会保険 + sb.add("soc_ins_name", + SchemaDefinition.Types.STRING, + SmartHRClient.BizEstablishment.class, + SmartHRClient.BizEstablishment.class, + (source, dest) -> dest.soc_ins_name = source, + (source) -> source.soc_ins_name, + null, + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE, NOT_RETURN_BY_DEFAULT + ); + + sb.add("soc_ins_tel_number", + SchemaDefinition.Types.STRING, + SmartHRClient.BizEstablishment.class, + SmartHRClient.BizEstablishment.class, + (source, dest) -> dest.soc_ins_tel_number = source, + (source) -> source.soc_ins_tel_number, + null, + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE, NOT_RETURN_BY_DEFAULT + ); + + sb.add("lab_ins_name", + SchemaDefinition.Types.STRING, + SmartHRClient.BizEstablishment.class, + SmartHRClient.BizEstablishment.class, + (source, dest) -> dest.lab_ins_name = source, + (source) -> source.lab_ins_name, + null, + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE, NOT_RETURN_BY_DEFAULT + ); + + // 労働保険 + sb.add("lab_ins_tel_number", + SchemaDefinition.Types.STRING, + SmartHRClient.BizEstablishment.class, + SmartHRClient.BizEstablishment.class, + (source, dest) -> dest.lab_ins_tel_number = source, + (source) -> source.lab_ins_tel_number, + null, + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE, NOT_RETURN_BY_DEFAULT + ); + + // Metadata (readonly) + sb.add("created_at", + SchemaDefinition.Types.DATETIME_STRING, + SmartHRClient.BizEstablishment.class, + SmartHRClient.BizEstablishment.class, + null, + (source) -> source.created_at, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + sb.add("updated_at", + SchemaDefinition.Types.DATETIME_STRING, + SmartHRClient.BizEstablishment.class, + SmartHRClient.BizEstablishment.class, + null, + (source) -> source.updated_at, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + + LOGGER.ok("The constructed job_title schema"); + + return sb; + } + + @Override + public SchemaDefinition getSchema() { + return schema; + } + + @Override + public Uid create(Set attributes) { + throw new UnsupportedOperationException("SmartHR API doesn't support creating biz_establishment"); + } + + @Override + public Set updateDelta(Uid uid, Set modifications, OperationOptions options) { + throw new UnsupportedOperationException("SmartHR API doesn't support creating biz_establishment"); + } + + @Override + public void delete(Uid uid, OperationOptions options) { + throw new UnsupportedOperationException("SmartHR API doesn't support creating biz_establishment"); + } + + @Override + public int getByUid(Uid uid, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + SmartHRClient.BizEstablishment dept = client.getBizEstablishment(uid, options, fetchFieldsSet); + + if (dept != null) { + resultsHandler.handle(toConnectorObject(schema, dept, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getByName(Name name, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + SmartHRClient.BizEstablishment dept = client.getBizEstablishment(name, options, fetchFieldsSet); + + if (dept != null) { + resultsHandler.handle(toConnectorObject(schema, dept, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getAll(ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + return client.getBizEstablishments((crew) -> resultsHandler.handle(toConnectorObject(schema, crew, returnAttributesSet, allowPartialAttributeValues)), + options, fetchFieldsSet, pageSize, pageOffset); + } +} diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRClient.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRClient.java new file mode 100644 index 0000000..c1de6a3 --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRClient.java @@ -0,0 +1,314 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import org.identityconnectors.framework.common.exceptions.AlreadyExistsException; +import org.identityconnectors.framework.common.objects.Name; +import org.identityconnectors.framework.common.objects.OperationOptions; +import org.identityconnectors.framework.common.objects.Uid; + +import java.util.List; +import java.util.Set; + +public interface SmartHRClient { + void test(); + + List schema(); + + default String getCustomSchemaGroupEndpointURL(SmartHRConfiguration configuration) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/crew_custom_field_template_groups", url); + } + + default String getCustomSchemaFieldEndpointURL(SmartHRConfiguration configuration) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/crew_custom_field_templates", url); + } + + default String getCrewEndpointURL(SmartHRConfiguration configuration) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/crews", url); + } + + default String getCrewEndpointURL(SmartHRConfiguration configuration, Uid uid) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/crews/%s", url, uid.getUidValue()); + } + + default String getDeptEndpointURL(SmartHRConfiguration configuration) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/departments", url); + } + + default String getDeptEndpointURL(SmartHRConfiguration configuration, Uid uid) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/departments/%s", url, uid.getUidValue()); + } + + default String getEmpTypeEndpointURL(SmartHRConfiguration configuration) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/employment_types", url); + } + + default String getEmpTypeEndpointURL(SmartHRConfiguration configuration, Uid uid) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/employment_types/%s", url, uid.getUidValue()); + } + + default String getEmpTypeEndpointURL(SmartHRConfiguration configuration, Name name) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/employment_types/%s", url, name.getNameValue()); + } + + default String getJobTitleEndpointURL(SmartHRConfiguration configuration) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/job_titles", url); + } + + default String getJobTitleEndpointURL(SmartHRConfiguration configuration, Uid uid) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/job_titles/%s", url, uid.getUidValue()); + } + + default String getJobTitleEndpointURL(SmartHRConfiguration configuration, Name name) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/job_titles/%s", url, name.getNameValue()); + } + + default String getCompanyEndpointURL(SmartHRConfiguration configuration) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/companies", url); + } + + default String getBizEstablishmentEndpointURL(SmartHRConfiguration configuration) { + String url = configuration.getEndpointURL(); + return String.format("%sapi/v1/biz_establishments", url); + } + + void close(); + + // Crew + + Uid createCrew(Crew newCrew) throws AlreadyExistsException; + + Crew getCrew(Uid uid, OperationOptions options, Set fetchFieldsSet); + + Crew getCrew(Name name, OperationOptions options, Set fetchFieldsSet); + + void updateCrew(Uid uid, Crew update); + + void deleteCrew(Uid uid, OperationOptions options); + + int getCrews(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset); + + // Department + + Uid createDepartment(Department newCrew) throws AlreadyExistsException; + + Department getDepartment(Uid uid, OperationOptions options, Set fetchFieldsSet); + + Department getDepartment(Name name, OperationOptions options, Set fetchFieldsSet); + + void updateDepartment(Uid uid, Department update); + + void deleteDepartment(Uid uid, OperationOptions options); + + int getDepartments(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset); + + // EmploymentType + + Uid createEmploymentType(EmploymentType newCrew) throws AlreadyExistsException; + + EmploymentType getEmploymentType(Uid uid, OperationOptions options, Set fetchFieldsSet); + + EmploymentType getEmploymentType(Name name, OperationOptions options, Set fetchFieldsSet); + + void updateEmploymentType(Uid uid, EmploymentType update); + + void deleteEmploymentType(Uid uid, OperationOptions options); + + int getEmploymentTypes(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset); + + // JobTitle + + Uid createJobTitle(JobTitle newCrew) throws AlreadyExistsException; + + JobTitle getJobTitle(Uid uid, OperationOptions options, Set fetchFieldsSet); + + JobTitle getJobTitle(Name name, OperationOptions options, Set fetchFieldsSet); + + void updateJobTitle(Uid uid, JobTitle update); + + void deleteJobTitle(Uid uid, OperationOptions options); + + int getJobTitles(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset); + + // Company + + Company getCompany(Uid uid, OperationOptions options, Set fetchFieldsSet); + + Company getCompany(Name name, OperationOptions options, Set fetchFieldsSet); + + int getCompanies(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset); + + // Biz Establishment + + BizEstablishment getBizEstablishment(Uid uid, OperationOptions options, Set fetchFieldsSet); + + BizEstablishment getBizEstablishment(Name name, OperationOptions options, Set fetchFieldsSet); + + int getBizEstablishments(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset); + + // JSON Representation + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + class Crew { + public String id; + public String emp_code; + + public String emp_status; + public String birth_at; + public List department_ids; + public List departments; + public String last_name; + public String first_name; + public String last_name_yomi; + public String first_name_yomi; + public String business_last_name; + public String business_first_name; + public String business_last_name_yomi; + public String business_first_name_yomi; + public String gender; + public String email; + public String department; + public String tel_number; + public String contract_type; + public String contract_start_on; + public String contract_end_on; + public String contract_renewal_type; + public String created_at; + public String updated_at; + public String entered_at; + public String resigned_at; + public String biz_establishment_id; + public String employment_type_id; + public EmploymentType employment_type; + public String position; + public String occupation; + public List custom_fields; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + class EmploymentType { + public String id; + public String name; + public String preset_type; + public String created_at; + public String updated_at; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + class Department { + public String id; + public String name; + public String code; + public String parent_id; + public Department parent; + public String created_at; + public String updated_at; + public Integer position; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + class JobTitle { + public String id; + public String name; + public Integer rank; + public String created_at; + public String updated_at; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + class Company { + public String id; + public String name; + public String tel_number; + public String created_at; + public String updated_at; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + class BizEstablishment { + public String id; + public String name; + public String soc_ins_name; + public String soc_ins_tel_number; + public String lab_ins_name; + public String lab_ins_tel_number; + public String created_at; + public String updated_at; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + class CustomField { + public String template_id; + public String value; + public Template template; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + class Template { + public String id; + public String name; + public String type; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + class CrewCustomField { + public String id; + public String name; + public String type; + public String group_id; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + class ErrorResponse { + public int code; + public String type; + public String message; + public List errors; + + public boolean isAlreadyExists() { + return code == 1; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + class ErrorDetail { + public String message; + public String resource; + public String field; + } +} \ No newline at end of file diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRCompanyHandler.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRCompanyHandler.java new file mode 100644 index 0000000..09e0209 --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRCompanyHandler.java @@ -0,0 +1,159 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.UnknownUidException; +import org.identityconnectors.framework.common.objects.*; + +import java.util.Set; + +import static jp.openstandia.connector.smarthr.SchemaDefinition.SchemaOption.*; + +public class SmartHRCompanyHandler implements SmartHRObjectHandler { + + public static final ObjectClass COMPANY_OBJECT_CLASS = new ObjectClass("company"); + + private static final Log LOGGER = Log.getLog(SmartHRCompanyHandler.class); + + private final SmartHRConfiguration configuration; + private final SmartHRClient client; + private final SchemaDefinition schema; + + public SmartHRCompanyHandler(SmartHRConfiguration configuration, SmartHRClient client, SchemaDefinition schema) { + this.configuration = configuration; + this.client = client; + this.schema = schema; + } + + public static SchemaDefinition.Builder createSchema() { + SchemaDefinition.Builder sb = SchemaDefinition.newBuilder(COMPANY_OBJECT_CLASS); + + // __UID__ + // The id for the company. Must be unique within the SmartHR tenant and unchangeable. + // Also, it's UUID (case-insensitive). + // We can't use "id" for the schema because of conflict in midpoint. + sb.addUid("company_id", + SchemaDefinition.Types.UUID, + SmartHRClient.Company.class, + SmartHRClient.Company.class, + null, + (source) -> source.id, + "id", + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE + ); + + // __NAME__ + sb.addName("name", + SchemaDefinition.Types.STRING, + SmartHRClient.Company.class, + SmartHRClient.Company.class, + (source, dest) -> dest.name = source, + (source) -> source.name, + null, + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE + ); + + // 基本情報 + sb.add("tel_number", + SchemaDefinition.Types.STRING, + SmartHRClient.Company.class, + SmartHRClient.Company.class, + (source, dest) -> dest.tel_number = source, + (source) -> source.tel_number, + null, + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE + ); + + // Metadata (readonly) + sb.add("created_at", + SchemaDefinition.Types.DATETIME_STRING, + SmartHRClient.Company.class, + SmartHRClient.Company.class, + null, + (source) -> source.created_at, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + sb.add("updated_at", + SchemaDefinition.Types.DATETIME_STRING, + SmartHRClient.Company.class, + SmartHRClient.Company.class, + null, + (source) -> source.updated_at, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + + LOGGER.ok("The constructed job_title schema"); + + return sb; + } + + @Override + public SchemaDefinition getSchema() { + return schema; + } + + @Override + public Uid create(Set attributes) { + throw new UnsupportedOperationException("SmartHR API doesn't support creating company"); + } + + @Override + public Set updateDelta(Uid uid, Set modifications, OperationOptions options) { + throw new UnsupportedOperationException("SmartHR API doesn't support updating company"); + } + + @Override + public void delete(Uid uid, OperationOptions options) { + throw new UnsupportedOperationException("SmartHR API doesn't support deleting company"); + } + + @Override + public int getByUid(Uid uid, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + SmartHRClient.Company dept = client.getCompany(uid, options, fetchFieldsSet); + + if (dept != null) { + resultsHandler.handle(toConnectorObject(schema, dept, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getByName(Name name, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + SmartHRClient.Company dept = client.getCompany(name, options, fetchFieldsSet); + + if (dept != null) { + resultsHandler.handle(toConnectorObject(schema, dept, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getAll(ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + return client.getCompanies((company) -> resultsHandler.handle(toConnectorObject(schema, company, returnAttributesSet, allowPartialAttributeValues)), + options, fetchFieldsSet, pageSize, pageOffset); + } +} diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRConfiguration.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRConfiguration.java new file mode 100644 index 0000000..a445fdd --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRConfiguration.java @@ -0,0 +1,189 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.common.security.GuardedString; +import org.identityconnectors.framework.common.exceptions.ConfigurationException; +import org.identityconnectors.framework.spi.AbstractConfiguration; +import org.identityconnectors.framework.spi.ConfigurationProperty; + +public class SmartHRConfiguration extends AbstractConfiguration { + + private String endpointURL; + private GuardedString apiAccessToken; + private String httpProxyHost; + private Integer httpProxyPort = 3128; + private String httpProxyUser; + private GuardedString httpProxyPassword; + private Integer defaultQueryPageSize = 50; + private Integer connectionTimeoutInSeconds = 10; + private Integer readTimeoutInSeconds = 10; + private Integer writeTimeoutInSeconds = 10; + + @ConfigurationProperty( + order = 1, + displayMessageKey = "SmartHR API URL", + helpMessageKey = "SmartHR API URL which is connected from this connector." + + " e.g. https://.smarthr.jp or https://.daruma.space", + required = true, + confidential = false) + public String getEndpointURL() { + if (endpointURL != null && !endpointURL.endsWith("/")) { + return endpointURL + "/"; + } + return endpointURL; + } + + public void setEndpointURL(String endpointURL) { + this.endpointURL = endpointURL; + } + + @ConfigurationProperty( + order = 2, + displayMessageKey = "SmartHR API Access Token", + helpMessageKey = "Access token for the API authentication.", + required = true, + confidential = true) + public GuardedString getApiAccessToken() { + return apiAccessToken; + } + + public void setApiAccessToken(GuardedString apiAccessToken) { + this.apiAccessToken = apiAccessToken; + } + + @ConfigurationProperty( + order = 3, + displayMessageKey = "HTTP Proxy Host", + helpMessageKey = "Hostname for the HTTP Proxy.", + required = false, + confidential = false) + public String getHttpProxyHost() { + return httpProxyHost; + } + + public void setHttpProxyHost(String httpProxyHost) { + this.httpProxyHost = httpProxyHost; + } + + @ConfigurationProperty( + order = 4, + displayMessageKey = "HTTP Proxy Port", + helpMessageKey = "Port for the HTTP Proxy. (Default: 3128)", + required = false, + confidential = false) + public int getHttpProxyPort() { + return httpProxyPort; + } + + public void setHttpProxyPort(int httpProxyPort) { + this.httpProxyPort = httpProxyPort; + } + + @ConfigurationProperty( + order = 5, + displayMessageKey = "HTTP Proxy User", + helpMessageKey = "Username for the HTTP Proxy Authentication.", + required = false, + confidential = false) + public String getHttpProxyUser() { + return httpProxyUser; + } + + public void setHttpProxyUser(String httpProxyUser) { + this.httpProxyUser = httpProxyUser; + } + + @ConfigurationProperty( + order = 6, + displayMessageKey = "HTTP Proxy Password", + helpMessageKey = "Password for the HTTP Proxy Authentication.", + required = false, + confidential = true) + public GuardedString getHttpProxyPassword() { + return httpProxyPassword; + } + + public void setHttpProxyPassword(GuardedString httpProxyPassword) { + this.httpProxyPassword = httpProxyPassword; + } + + @ConfigurationProperty( + order = 7, + displayMessageKey = "Default Query Page Size", + helpMessageKey = "Number of results to return per page. (Default: 50)", + required = false, + confidential = false) + public int getDefaultQueryPageSize() { + return defaultQueryPageSize; + } + + public void setDefaultQueryPageSize(int defaultQueryPageSize) { + this.defaultQueryPageSize = defaultQueryPageSize; + } + + @ConfigurationProperty( + order = 8, + displayMessageKey = "Connection Timeout (in seconds)", + helpMessageKey = "Connection timeout when connecting to SmartHR. (Default: 10)", + required = false, + confidential = false) + public int getConnectionTimeoutInSeconds() { + return connectionTimeoutInSeconds; + } + + public void setConnectionTimeoutInSeconds(int connectionTimeoutInSeconds) { + this.connectionTimeoutInSeconds = connectionTimeoutInSeconds; + } + + @ConfigurationProperty( + order = 9, + displayMessageKey = "Read Timeout (in seconds)", + helpMessageKey = "Read timeout when fetching data from SmartHR. (Default: 10)", + required = false, + confidential = false) + public int getReadTimeoutInSeconds() { + return readTimeoutInSeconds; + } + + public void setReadTimeoutInSeconds(int writeTimeoutInSeconds) { + this.writeTimeoutInSeconds = writeTimeoutInSeconds; + } + + @ConfigurationProperty( + order = 10, + displayMessageKey = "Write Timeout (in seconds)", + helpMessageKey = "Write timeout when fetching data from SmartHR. (Default: 10)", + required = false, + confidential = false) + public int getWriteTimeoutInSeconds() { + return writeTimeoutInSeconds; + } + + public void setWriteTimeoutInSeconds(int writeTimeoutInSeconds) { + this.writeTimeoutInSeconds = writeTimeoutInSeconds; + } + + @Override + public void validate() { + if (endpointURL == null) { + throw new ConfigurationException("SmartHR Endpoint URL is required"); + } + if (apiAccessToken == null) { + throw new ConfigurationException("SmartHR API Password is required"); + } + } +} diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRConnector.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRConnector.java new file mode 100644 index 0000000..5b8178a --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRConnector.java @@ -0,0 +1,288 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import jp.openstandia.connector.smarthr.rest.SmartHRRESTClient; +import okhttp3.*; +import org.identityconnectors.common.StringUtil; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.common.security.GuardedString; +import org.identityconnectors.framework.common.exceptions.*; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.FilterTranslator; +import org.identityconnectors.framework.spi.*; +import org.identityconnectors.framework.spi.operations.*; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static jp.openstandia.connector.smarthr.SmartHRUtils.*; + +@ConnectorClass(configurationClass = SmartHRConfiguration.class, displayNameKey = "SmartHR Connector") +public class SmartHRConnector implements PoolableConnector, CreateOp, UpdateDeltaOp, DeleteOp, SchemaOp, TestOp, SearchOp, InstanceNameAware { + + private static final Log LOG = Log.getLog(SmartHRConnector.class); + + protected SmartHRConfiguration configuration; + protected SmartHRClient client; + + private SmartHRSchema cachedSchema; + private String instanceName; + + @Override + public Configuration getConfiguration() { + return configuration; + } + + @Override + public void init(Configuration configuration) { + this.configuration = (SmartHRConfiguration) configuration; + + try { + authenticateResource(); + } catch (RuntimeException e) { + throw processRuntimeException(e); + } + + LOG.ok("Connector {0} successfully initialized", getClass().getName()); + } + + protected void authenticateResource() { + OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder(); + okHttpBuilder.connectTimeout(configuration.getConnectionTimeoutInSeconds(), TimeUnit.SECONDS); + okHttpBuilder.readTimeout(configuration.getReadTimeoutInSeconds(), TimeUnit.SECONDS); + okHttpBuilder.writeTimeout(configuration.getWriteTimeoutInSeconds(), TimeUnit.SECONDS); + okHttpBuilder.addInterceptor(getInterceptor(configuration.getApiAccessToken())); + + // Setup http proxy aware httpClient + if (StringUtil.isNotEmpty(configuration.getHttpProxyHost())) { + okHttpBuilder.proxy(new Proxy(Proxy.Type.HTTP, + new InetSocketAddress(configuration.getHttpProxyHost(), configuration.getHttpProxyPort()))); + + if (StringUtil.isNotEmpty(configuration.getHttpProxyUser()) && configuration.getHttpProxyPassword() != null) { + configuration.getHttpProxyPassword().access(c -> { + okHttpBuilder.proxyAuthenticator((Route route, Response response) -> { + String credential = Credentials.basic(configuration.getHttpProxyUser(), String.valueOf(c)); + return response.request().newBuilder() + .header("Proxy-Authorization", credential) + .build(); + }); + }); + } + } + + OkHttpClient httpClient = okHttpBuilder.build(); + + client = new SmartHRRESTClient(instanceName, configuration, httpClient); + + // Verify we can access the SmartHR API + client.test(); + } + + private Interceptor getInterceptor(GuardedString accessToken) { + return new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + Request.Builder builder = chain.request().newBuilder() + .addHeader("Accept", "application/json"); + accessToken.access(c -> { + builder.addHeader("Authorization", "Bearer " + String.valueOf(c)); + }); + return chain.proceed(builder.build()); + } + }; + } + + @Override + public Schema schema() { + try { + List smarthrSchema = this.client.schema(); + cachedSchema = new SmartHRSchema(configuration, client, smarthrSchema); + return cachedSchema.schema; + + } catch (RuntimeException e) { + throw processRuntimeException(e); + } + } + + private SmartHRObjectHandler getSchemaHandler(ObjectClass objectClass) { + if (objectClass == null) { + throw new InvalidAttributeValueException("ObjectClass value not provided"); + } + + // Load schema map if it's not loaded yet + if (cachedSchema == null) { + schema(); + } + + SmartHRObjectHandler handler = cachedSchema.getSchemaHandler(objectClass); + + if (handler == null) { + throw new InvalidAttributeValueException("Unsupported object class " + objectClass); + } + + return handler; + } + + @Override + public Uid create(ObjectClass objectClass, Set createAttributes, OperationOptions options) { + if (createAttributes == null || createAttributes.isEmpty()) { + throw new InvalidAttributeValueException("Attributes not provided or empty"); + } + + try { + return getSchemaHandler(objectClass).create(createAttributes); + + } catch (RuntimeException e) { + throw processRuntimeException(e); + } + } + + @Override + public Set updateDelta(ObjectClass objectClass, Uid uid, Set modifications, OperationOptions options) { + if (uid == null) { + throw new InvalidAttributeValueException("uid not provided"); + } + + try { + return getSchemaHandler(objectClass).updateDelta(uid, modifications, options); + + } catch (UnknownUidException e) { + LOG.warn("Not found object when updating. objectClass: {0}, uid: {1}", objectClass, uid); + throw processRuntimeException(e); + + } catch (RuntimeException e) { + throw processRuntimeException(e); + } + } + + @Override + public void delete(ObjectClass objectClass, Uid uid, OperationOptions options) { + if (uid == null) { + throw new InvalidAttributeValueException("uid not provided"); + } + + try { + getSchemaHandler(objectClass).delete(uid, options); + + } catch (UnknownUidException e) { + LOG.warn("Not found object when deleting. objectClass: {0}, uid: {1}", objectClass, uid); + throw processRuntimeException(e); + + } catch (RuntimeException e) { + throw processRuntimeException(e); + } + } + + @Override + public FilterTranslator createFilterTranslator(ObjectClass objectClass, OperationOptions options) { + return new SmartHRFilterTranslator(objectClass, options); + } + + @Override + public void executeQuery(ObjectClass objectClass, SmartHRFilter filter, ResultsHandler resultsHandler, OperationOptions options) { + SmartHRObjectHandler schemaHandler = getSchemaHandler(objectClass); + SchemaDefinition schema = schemaHandler.getSchema(); + + int pageSize = resolvePageSize(configuration, options); + int pageOffset = resolvePageOffset(options); + + // Create full attributesToGet by RETURN_DEFAULT_ATTRIBUTES + ATTRIBUTES_TO_GET + Map attributesToGet = createFullAttributesToGet(schema, options); + Set returnAttributesSet = attributesToGet.keySet(); + Set fetchFieldSet = attributesToGet.values().stream().collect(Collectors.toSet()); + + boolean allowPartialAttributeValues = shouldAllowPartialAttributeValues(options); + + int total = 0; + + if (filter != null) { + if (filter.isByUid()) { + total = schemaHandler.getByUid((Uid) filter.attributeValue, resultsHandler, options, + returnAttributesSet, fetchFieldSet, + allowPartialAttributeValues, pageSize, pageOffset); + } else if (filter.isByName()) { + total = schemaHandler.getByName((Name) filter.attributeValue, resultsHandler, options, + returnAttributesSet, fetchFieldSet, + allowPartialAttributeValues, pageSize, pageOffset); + } + // No result + } else { + total = schemaHandler.getAll(resultsHandler, options, + returnAttributesSet, fetchFieldSet, + allowPartialAttributeValues, pageSize, pageOffset); + } + + if (resultsHandler instanceof SearchResultsHandler && + pageOffset > 0) { + + int remaining = total - (pageSize * pageOffset); + + SearchResultsHandler searchResultsHandler = (SearchResultsHandler) resultsHandler; + SearchResult searchResult = new SearchResult(null, remaining); + searchResultsHandler.handleResult(searchResult); + } + } + + @Override + public void test() { + try { + dispose(); + authenticateResource(); + } catch (RuntimeException e) { + throw processRuntimeException(e); + } + } + + @Override + public void dispose() { + client.close(); + this.client = null; + } + + @Override + public void checkAlive() { + // Do nothing + } + + @Override + public void setInstanceName(String instanceName) { + this.instanceName = instanceName; + } + + protected ConnectorException processRuntimeException(RuntimeException e) { + if (e instanceof ConnectorException) { + // Write error log because IDM might not write full stack trace + // It's hard to debug the error + if (e instanceof AlreadyExistsException) { + LOG.warn(e, "Detected the object already exists"); + } else { + LOG.error(e, "Detected SmartHR connector error"); + } + return (ConnectorException) e; + } + + LOG.error(e, "Detected SmartHR connector unexpected error"); + + return new ConnectorIOException(e); + } +} diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRCrewHandler.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRCrewHandler.java new file mode 100644 index 0000000..d244dbe --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRCrewHandler.java @@ -0,0 +1,549 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.identityconnectors.common.StringUtil; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.UnknownUidException; +import org.identityconnectors.framework.common.objects.*; + +import java.math.BigDecimal; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static jp.openstandia.connector.smarthr.SchemaDefinition.SchemaOption.*; +import static jp.openstandia.connector.smarthr.SmartHRUtils.toZoneDateTime; + +public class SmartHRCrewHandler implements SmartHRObjectHandler { + + public static final ObjectClass CREW_OBJECT_CLASS = new ObjectClass("crew"); + + private static final Log LOGGER = Log.getLog(SmartHRCrewHandler.class); + + private final SmartHRConfiguration configuration; + private final SmartHRClient client; + private final SchemaDefinition schema; + + public SmartHRCrewHandler(SmartHRConfiguration configuration, SmartHRClient client, + SchemaDefinition schema) { + this.configuration = configuration; + this.client = client; + this.schema = schema; + } + + private static final ObjectMapper mapper = new ObjectMapper(); + + public static SchemaDefinition.Builder createSchema(List schema) { + SchemaDefinition.Builder sb = SchemaDefinition.newBuilder(CREW_OBJECT_CLASS); + + // __UID__ + // The id for the crew. Must be unique within the SmartHR tenant and unchangeable. + // Also, it's UUID (case-insensitive). + // We can't use "id" for the schema because of conflict in midpoint. + sb.addUid("crew_id", + SchemaDefinition.Types.UUID, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + null, + (source) -> source.id, + "id", + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE + ); + + // emp_code (__NAME__) + // The emp_code for the user. Must be unique within the SmartHR tenant and changeable. + // This is NOT required attribute in the tenant. If IDM doesn't provide, use __UID__ as __NAME__. + // Also, it's case-sensitive. + sb.addName("emp_code", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.emp_code = source, + (source) -> StringUtil.isEmpty(source.emp_code) ? source.id : source.emp_code, + null + ); + + // 基本情報 + sb.add("last_name", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.last_name = source, + (source) -> source.last_name, + null + ); + sb.add("first_name", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.first_name = source, + (source) -> source.first_name, + null + ); + sb.add("last_name_yomi", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.last_name_yomi = source, + (source) -> source.last_name_yomi, + null + ); + sb.add("first_name_yomi", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.first_name_yomi = source, + (source) -> source.first_name_yomi, + null + ); + + sb.add("business_last_name", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.business_last_name = source, + (source) -> source.business_last_name, + null + ); + sb.add("business_first_name", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.business_first_name = source, + (source) -> source.business_first_name, + null + ); + sb.add("business_last_name_yomi", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.business_last_name_yomi = source, + (source) -> source.business_last_name_yomi, + null + ); + sb.add("business_first_name_yomi", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.business_first_name_yomi = source, + (source) -> source.business_first_name_yomi, + null + ); + + sb.add("birth_at", + SchemaDefinition.Types.DATE_STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.birth_at = source, + (source) -> source.birth_at, + null + ); + sb.add("gender", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.gender = source, + (source) -> source.gender, + null + ); + sb.add("email", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.email = source, + (source) -> source.email, + null + ); + + // 入退社情報 + sb.add("emp_status", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.emp_status = source, + (source) -> source.emp_status, + null, + REQUIRED + ); + sb.add("entered_at", + SchemaDefinition.Types.DATE_STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.entered_at = source, + (source) -> source.entered_at, + null + ); + sb.add("resigned_at", + SchemaDefinition.Types.DATE_STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.resigned_at = source, + (source) -> source.resigned_at, + null + ); + + // 業務情報 + // emp_code is __NAME__ +// sb.add("emp_code", +// SchemaBuilder.Types.STRING, +// SmartHRClient.SmartHRCrewRepresentation.class, +// SmartHRClient.SmartHRCrewRepresentation.class, +// (source, dest) -> dest.emp_code = source, +// (source) -> source.emp_code +// ); + sb.add("biz_establishment_id", + SchemaDefinition.Types.UUID, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.biz_establishment_id = source, + (source) -> source.biz_establishment_id, + null + ); + sb.add("employment_type.id", + SchemaDefinition.Types.UUID, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.employment_type_id = source, + (source) -> source.employment_type != null ? source.employment_type.id : null, + "employment_type" + ); + // readonly + sb.add("employment_type.name", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + null, + (source) -> source.employment_type != null ? source.employment_type.name : null, + "employment_type", + NOT_CREATABLE, NOT_UPDATABLE + ); + // readonly + sb.add("employment_type.preset_type", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + null, + (source) -> source.employment_type != null ? source.employment_type.preset_type : null, + "employment_type", + NOT_CREATABLE, NOT_UPDATABLE + ); + sb.add("position", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.position = source, + (source) -> source.position, + null + ); + sb.add("occupation", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.occupation = source, + (source) -> source.occupation, + null + ); + + // 部署情報 + // readonly + sb.add("department", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + null, + (source) -> source.department, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + // Association + sb.addAsMultiple("departments", + SchemaDefinition.Types.UUID, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.department_ids = source, + (add, dest) -> dest.department_ids.addAll(add), + (remove, dest) -> dest.department_ids.removeAll(remove), + (source) -> source.departments != null ? source.departments.stream().map(d -> d.id).collect(Collectors.toList()) : null, + null + ); + // readonly + sb.addAsMultiple("raw_departments", + SchemaDefinition.Types.JSON, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + null, + null, + null, + (source) -> { + if (source.departments == null) { + return null; + } + List depts = source.departments.stream() + .map(d -> { + // TODO use jackson native feature + try { + return mapper.writeValueAsString(d); + } catch (JsonProcessingException ignore) { + return null; + } + }) + .filter(d -> d != null) + .collect(Collectors.toList()); + return depts; + }, + "departments", + NOT_CREATABLE, NOT_UPDATABLE, NOT_RETURN_BY_DEFAULT + ); + + // 現住所と連絡先 + sb.add("tel_number", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.tel_number = source, + (source) -> source.tel_number, + null, + NOT_RETURN_BY_DEFAULT + ); + + // 雇用契約情報 + sb.add("contract_type", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.contract_type = source, + (source) -> source.contract_type, + null + ); + sb.add("contract_start_on", + SchemaDefinition.Types.DATE_STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.contract_start_on = source, + (source) -> source.contract_start_on, + null + ); + sb.add("contract_end_on", + SchemaDefinition.Types.DATE_STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.contract_end_on = source, + (source) -> source.contract_end_on, + null + ); + sb.add("contract_renewal_type", + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> dest.contract_renewal_type = source, + (source) -> source.contract_renewal_type, + null + ); + + // Metadata (readonly) + sb.add("created_at", + SchemaDefinition.Types.DATETIME_STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + null, + (source) -> source.created_at, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + sb.add("updated_at", + SchemaDefinition.Types.DATETIME_STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + null, + (source) -> source.updated_at, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + + // Custom Fields + for (SmartHRClient.CrewCustomField field : schema) { + final String name = "custom." + field.id; + + switch (field.type) { + case "string": + case "text": + case "enum": + sb.add(name, + SchemaDefinition.Types.STRING, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> { + SmartHRClient.CustomField value = new SmartHRClient.CustomField(); + value.template_id = field.id; + value.value = source; + dest.custom_fields.add(value); + }, + (source) -> { + Optional value = source.custom_fields.stream() + .filter(f -> f.template.id.equals(field.id)) + .findFirst(); + if (value.isPresent()) { + return value.get().value; + } + return null; + }, + "custom_fields" + ); + break; + case "decimal": + sb.add(name, + SchemaDefinition.Types.BIG_DECIMAL, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> { + SmartHRClient.CustomField value = new SmartHRClient.CustomField(); + value.template_id = field.id; + value.value = source.toPlainString(); + dest.custom_fields.add(value); + }, + (source) -> { + Optional value = source.custom_fields.stream() + .filter(f -> f.template.id.equals(field.id)) + .findFirst(); + if (value.isPresent()) { + return new BigDecimal(value.get().value); + } + return null; + }, + "custom_fields" + ); + break; + case "date": + sb.add(name, + SchemaDefinition.Types.DATE, + SmartHRClient.Crew.class, + SmartHRClient.Crew.class, + (source, dest) -> { + SmartHRClient.CustomField value = new SmartHRClient.CustomField(); + value.template_id = field.id; + value.value = source.format(DateTimeFormatter.ISO_LOCAL_DATE); + dest.custom_fields.add(value); + }, + (source) -> { + Optional value = source.custom_fields.stream() + .filter(f -> f.template.id.equals(field.id)) + .findFirst(); + if (value.isPresent()) { + return toZoneDateTime(value.get().value); + } + return null; + }, + "custom_fields" + ); + break; + case "file": + default: + LOGGER.info("Not supported crew custom field type: {0}", field.type); + continue; + } + } + + LOGGER.ok("The constructed crew schema"); + + return sb; + } + + @Override + public SchemaDefinition getSchema() { + return schema; + } + + @Override + public Uid create(Set attributes) { + SmartHRClient.Crew dest = new SmartHRClient.Crew(); + + schema.apply(attributes, dest); + + Uid newUid = client.createCrew(dest); + + return newUid; + } + + @Override + public Set updateDelta(Uid uid, Set modifications, OperationOptions options) { + // To apply diff for multiple values, we need to fetch the current object + SmartHRClient.Crew current = client.getCrew(uid, options, null); + + if (current == null) { + throw new UnknownUidException(String.format("Not found crew. id: %s", uid.getUidValue())); + } + + SmartHRClient.Crew dest = new SmartHRClient.Crew(); + if (current.departments == null) { + dest.department_ids = new ArrayList<>(); + } else { + dest.department_ids = current.departments.stream().map(d -> d.id).collect(Collectors.toList()); + } + + schema.applyDelta(modifications, dest); + + client.updateCrew(uid, dest); + + return null; + } + + @Override + public void delete(Uid uid, OperationOptions options) { + client.deleteCrew(uid, options); + } + + @Override + public int getByUid(Uid uid, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + SmartHRClient.Crew crew = client.getCrew(uid, options, fetchFieldsSet); + + if (crew != null) { + resultsHandler.handle(toConnectorObject(schema, crew, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getByName(Name name, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + SmartHRClient.Crew user = client.getCrew(name, options, fetchFieldsSet); + + if (user != null) { + resultsHandler.handle(toConnectorObject(schema, user, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getAll(ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + return client.getCrews((crew) -> resultsHandler.handle(toConnectorObject(schema, crew, returnAttributesSet, allowPartialAttributeValues)), + options, fetchFieldsSet, pageSize, pageOffset); + } +} diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRDepartmentHandler.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRDepartmentHandler.java new file mode 100644 index 0000000..56fbefa --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRDepartmentHandler.java @@ -0,0 +1,199 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.common.StringUtil; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.UnknownUidException; +import org.identityconnectors.framework.common.objects.*; + +import java.util.Set; + +import static jp.openstandia.connector.smarthr.SchemaDefinition.SchemaOption.*; + +public class SmartHRDepartmentHandler implements SmartHRObjectHandler { + + public static final ObjectClass DEPARTMENT_OBJECT_CLASS = new ObjectClass("department"); + + private static final Log LOGGER = Log.getLog(SmartHRDepartmentHandler.class); + + private final SmartHRConfiguration configuration; + private final SmartHRClient client; + private final SchemaDefinition schema; + + public SmartHRDepartmentHandler(SmartHRConfiguration configuration, SmartHRClient client, SchemaDefinition schema) { + this.configuration = configuration; + this.client = client; + this.schema = schema; + } + + public static SchemaDefinition.Builder createSchema() { + SchemaDefinition.Builder sb = SchemaDefinition.newBuilder(DEPARTMENT_OBJECT_CLASS); + + // __UID__ + // The id for the department. Must be unique within the SmartHR tenant and unchangeable. + // Also, it's UUID (case-insensitive). + // We can't use "id" for the schema because of conflict in midpoint. + sb.addUid("department_id", + SchemaDefinition.Types.UUID, + SmartHRClient.Department.class, + SmartHRClient.Department.class, + null, + (source) -> source.id, + "id", + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE + ); + + // code (__NAME__) + // The code for the department. Must be unique within the SmartHR tenant and changeable. + // This is NOT required attribute in the tenant. If IDM doesn't provide, use __UID__ as __NAME__. + // Also, it's case-sensitive. + sb.addName("code", + SchemaDefinition.Types.STRING, + SmartHRClient.Department.class, + SmartHRClient.Department.class, + (source, dest) -> dest.code = source, + (source) -> StringUtil.isEmpty(source.code) ? source.id : source.code, + null + ); + + sb.add("name", + SchemaDefinition.Types.STRING, + SmartHRClient.Department.class, + SmartHRClient.Department.class, + (source, dest) -> dest.name = source, + (source) -> source.name, + null, + REQUIRED + ); + + sb.add("parent_id", + SchemaDefinition.Types.UUID, + SmartHRClient.Department.class, + SmartHRClient.Department.class, + (source, dest) -> dest.parent_id = source, + (source) -> source.parent != null ? source.parent.id : null, + null + ); + + // Metadata (readonly) + sb.add("position", + SchemaDefinition.Types.INTEGER, + SmartHRClient.Department.class, + SmartHRClient.Department.class, + (source, dest) -> dest.position = source, + (source) -> source.position, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + sb.add("created_at", + SchemaDefinition.Types.DATETIME_STRING, + SmartHRClient.Department.class, + SmartHRClient.Department.class, + null, + (source) -> source.created_at, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + sb.add("updated_at", + SchemaDefinition.Types.DATETIME_STRING, + SmartHRClient.Department.class, + SmartHRClient.Department.class, + null, + (source) -> source.updated_at, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + + LOGGER.ok("The constructed department schema"); + + return sb; + } + + + @Override + public SchemaDefinition getSchema() { + return schema; + } + + @Override + public Uid create(Set attributes) { + SmartHRClient.Department dest = new SmartHRClient.Department(); + + schema.apply(attributes, dest); + + Uid newUid = client.createDepartment(dest); + + return newUid; + } + + @Override + public Set updateDelta(Uid uid, Set modifications, OperationOptions options) { + // To apply diff for multiple values, we need to fetch the current object + SmartHRClient.Department current = client.getDepartment(uid, options, null); + + if (current == null) { + throw new UnknownUidException(String.format("Not found crew. id: %s", uid.getUidValue())); + } + + SmartHRClient.Department dest = new SmartHRClient.Department(); + + schema.applyDelta(modifications, dest); + + client.updateDepartment(uid, dest); + + return null; + } + + @Override + public void delete(Uid uid, OperationOptions options) { + client.deleteDepartment(uid, options); + } + + @Override + public int getByUid(Uid uid, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + SmartHRClient.Department dept = client.getDepartment(uid, options, fetchFieldsSet); + + if (dept != null) { + resultsHandler.handle(toConnectorObject(schema, dept, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getByName(Name name, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + SmartHRClient.Department dept = client.getDepartment(name, options, fetchFieldsSet); + + if (dept != null) { + resultsHandler.handle(toConnectorObject(schema, dept, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getAll(ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + return client.getDepartments((dept) -> resultsHandler.handle(toConnectorObject(schema, dept, returnAttributesSet, allowPartialAttributeValues)), + options, fetchFieldsSet, pageSize, pageOffset); + } +} diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHREmploymentTypeHandler.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHREmploymentTypeHandler.java new file mode 100644 index 0000000..32d3edd --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHREmploymentTypeHandler.java @@ -0,0 +1,168 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.UnknownUidException; +import org.identityconnectors.framework.common.objects.*; + +import java.util.Set; + +import static jp.openstandia.connector.smarthr.SchemaDefinition.SchemaOption.*; + +public class SmartHREmploymentTypeHandler implements SmartHRObjectHandler { + + public static final ObjectClass EMPLOYMENT_TYPE_OBJECT_CLASS = new ObjectClass("employment_type"); + + private static final Log LOGGER = Log.getLog(SmartHREmploymentTypeHandler.class); + + private final SmartHRConfiguration configuration; + private final SmartHRClient client; + private final SchemaDefinition schema; + + public SmartHREmploymentTypeHandler(SmartHRConfiguration configuration, SmartHRClient client, SchemaDefinition schema) { + this.configuration = configuration; + this.client = client; + this.schema = schema; + } + + public static SchemaDefinition.Builder createSchema() { + SchemaDefinition.Builder sb = SchemaDefinition.newBuilder(EMPLOYMENT_TYPE_OBJECT_CLASS); + + // __UID__ + // The id for the employment_type. Must be unique within the SmartHR tenant and unchangeable. + // Also, it's UUID (case-insensitive). + // We can't use "id" for the schema because of conflict in midpoint. + sb.addUid("employment_type_id", + SchemaDefinition.Types.UUID, + SmartHRClient.EmploymentType.class, + SmartHRClient.EmploymentType.class, + null, + (source) -> source.id, + "id", + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE + ); + + // __NAME__ + sb.addName("name", + SchemaDefinition.Types.STRING, + SmartHRClient.EmploymentType.class, + SmartHRClient.EmploymentType.class, + (source, dest) -> dest.name = source, + (source) -> source.name, + null, + REQUIRED + ); + + // Metadata (readonly) + sb.add("created_at", + SchemaDefinition.Types.DATETIME_STRING, + SmartHRClient.EmploymentType.class, + SmartHRClient.EmploymentType.class, + null, + (source) -> source.created_at, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + sb.add("updated_at", + SchemaDefinition.Types.DATETIME_STRING, + SmartHRClient.EmploymentType.class, + SmartHRClient.EmploymentType.class, + null, + (source) -> source.updated_at, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + + LOGGER.ok("The constructed employment_type schema"); + + return sb; + } + + + @Override + public SchemaDefinition getSchema() { + return schema; + } + + @Override + public Uid create(Set attributes) { + SmartHRClient.EmploymentType dest = new SmartHRClient.EmploymentType(); + + schema.apply(attributes, dest); + + Uid newUid = client.createEmploymentType(dest); + + return newUid; + } + + @Override + public Set updateDelta(Uid uid, Set modifications, OperationOptions options) { + // To apply diff for multiple values, we need to fetch the current object + SmartHRClient.EmploymentType current = client.getEmploymentType(uid, options, null); + + if (current == null) { + throw new UnknownUidException(String.format("Not found employment_type. id: %s", uid.getUidValue())); + } + + SmartHRClient.EmploymentType dest = new SmartHRClient.EmploymentType(); + + schema.applyDelta(modifications, dest); + + client.updateEmploymentType(uid, dest); + + return null; + } + + @Override + public void delete(Uid uid, OperationOptions options) { + client.deleteEmploymentType(uid, options); + } + + @Override + public int getByUid(Uid uid, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + SmartHRClient.EmploymentType dept = client.getEmploymentType(uid, options, fetchFieldsSet); + + if (dept != null) { + resultsHandler.handle(toConnectorObject(schema, dept, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getByName(Name name, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + SmartHRClient.EmploymentType dept = client.getEmploymentType(name, options, fetchFieldsSet); + + if (dept != null) { + resultsHandler.handle(toConnectorObject(schema, dept, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getAll(ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + return client.getEmploymentTypes((empType) -> resultsHandler.handle(toConnectorObject(schema, empType, returnAttributesSet, allowPartialAttributeValues)), + options, fetchFieldsSet, pageSize, pageOffset); + } +} diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRFilter.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRFilter.java new file mode 100644 index 0000000..051bb2c --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRFilter.java @@ -0,0 +1,59 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.framework.common.objects.Attribute; +import org.identityconnectors.framework.common.objects.Name; +import org.identityconnectors.framework.common.objects.Uid; + +public class SmartHRFilter { + final String attributeName; + final FilterType filterType; + final Attribute attributeValue; + + public SmartHRFilter(String attributeName, FilterType filterType, Attribute attributeValue) { + this.attributeName = attributeName; + this.filterType = filterType; + this.attributeValue = attributeValue; + } + + public SmartHRFilter(String attributeName, FilterType filterType) { + this.attributeName = attributeName; + this.filterType = filterType; + this.attributeValue = null; + } + + public boolean isByName() { + return attributeName.equals(Name.NAME) && filterType == FilterType.EXACT_MATCH; + } + + public boolean isByUid() { + return attributeName.equals(Uid.NAME) && filterType == FilterType.EXACT_MATCH; + } + + public enum FilterType { + EXACT_MATCH; + } + + @Override + public String toString() { + return "SmartHRFilter{" + + "attributeName='" + attributeName + '\'' + + ", filterType=" + filterType + + ", attributeValue='" + attributeValue + '\'' + + '}'; + } +} diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRFilterTranslator.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRFilterTranslator.java new file mode 100644 index 0000000..dde0e96 --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRFilterTranslator.java @@ -0,0 +1,60 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.AbstractFilterTranslator; +import org.identityconnectors.framework.common.objects.filter.EqualsFilter; + +public class SmartHRFilterTranslator extends AbstractFilterTranslator { + + private static final Log LOG = Log.getLog(SmartHRFilterTranslator.class); + + private final OperationOptions options; + private final ObjectClass objectClass; + + public SmartHRFilterTranslator(ObjectClass objectClass, OperationOptions options) { + this.objectClass = objectClass; + this.options = options; + } + + @Override + protected SmartHRFilter createEqualsExpression(EqualsFilter filter, boolean not) { + if (not) { // no way (natively) to search for "NotEquals" + return null; + } + Attribute attr = filter.getAttribute(); + + if (attr instanceof Uid) { + Uid uid = (Uid) attr; + SmartHRFilter uidFilter = new SmartHRFilter(uid.getName(), + SmartHRFilter.FilterType.EXACT_MATCH, + uid); + return uidFilter; + } + if (attr instanceof Name) { + Name name = (Name) attr; + SmartHRFilter nameFilter = new SmartHRFilter(name.getName(), + SmartHRFilter.FilterType.EXACT_MATCH, + name); + return nameFilter; + } + + // Not supported searching by other attributes + return null; + } +} diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRJobTitleHandler.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRJobTitleHandler.java new file mode 100644 index 0000000..0b1eec6 --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRJobTitleHandler.java @@ -0,0 +1,177 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.UnknownUidException; +import org.identityconnectors.framework.common.objects.*; + +import java.util.Set; + +import static jp.openstandia.connector.smarthr.SchemaDefinition.SchemaOption.*; + +public class SmartHRJobTitleHandler implements SmartHRObjectHandler { + + public static final ObjectClass JOB_TITLE_OBJECT_CLASS = new ObjectClass("job_title"); + + private static final Log LOGGER = Log.getLog(SmartHRJobTitleHandler.class); + + private final SmartHRConfiguration configuration; + private final SmartHRClient client; + private final SchemaDefinition schema; + + public SmartHRJobTitleHandler(SmartHRConfiguration configuration, SmartHRClient client, SchemaDefinition schema) { + this.configuration = configuration; + this.client = client; + this.schema = schema; + } + + public static SchemaDefinition.Builder createSchema() { + SchemaDefinition.Builder sb = SchemaDefinition.newBuilder(JOB_TITLE_OBJECT_CLASS); + + // __UID__ + // The id for the employment_type. Must be unique within the SmartHR tenant and unchangeable. + // Also, it's UUID (case-insensitive). + // We can't use "id" for the schema because of conflict in midpoint. + sb.addUid("job_title_id", + SchemaDefinition.Types.UUID, + SmartHRClient.JobTitle.class, + SmartHRClient.JobTitle.class, + null, + (source) -> source.id, + "id", + REQUIRED, NOT_CREATABLE, NOT_UPDATABLE + ); + + // __NAME__ + sb.addName("name", + SchemaDefinition.Types.STRING, + SmartHRClient.JobTitle.class, + SmartHRClient.JobTitle.class, + (source, dest) -> dest.name = source, + (source) -> source.name, + null, + REQUIRED + ); + + sb.add("rank", + SchemaDefinition.Types.INTEGER, + SmartHRClient.JobTitle.class, + SmartHRClient.JobTitle.class, + (source, dest) -> dest.rank = source, + (source) -> source.rank, + null + + ); + + // Metadata (readonly) + sb.add("created_at", + SchemaDefinition.Types.DATETIME_STRING, + SmartHRClient.JobTitle.class, + SmartHRClient.JobTitle.class, + null, + (source) -> source.created_at, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + sb.add("updated_at", + SchemaDefinition.Types.DATETIME_STRING, + SmartHRClient.JobTitle.class, + SmartHRClient.JobTitle.class, + null, + (source) -> source.updated_at, + null, + NOT_CREATABLE, NOT_UPDATABLE + ); + + LOGGER.ok("The constructed job_title schema"); + + return sb; + } + + @Override + public SchemaDefinition getSchema() { + return schema; + } + + @Override + public Uid create(Set attributes) { + SmartHRClient.JobTitle dest = new SmartHRClient.JobTitle(); + + schema.apply(attributes, dest); + + Uid newUid = client.createJobTitle(dest); + + return newUid; + } + + @Override + public Set updateDelta(Uid uid, Set modifications, OperationOptions options) { + // To apply diff for multiple values, we need to fetch the current object + SmartHRClient.JobTitle current = client.getJobTitle(uid, options, null); + + if (current == null) { + throw new UnknownUidException(String.format("Not found job_title. id: %s", uid.getUidValue())); + } + + SmartHRClient.JobTitle dest = new SmartHRClient.JobTitle(); + + schema.applyDelta(modifications, dest); + + client.updateJobTitle(uid, dest); + + return null; + } + + @Override + public void delete(Uid uid, OperationOptions options) { + client.deleteJobTitle(uid, options); + } + + @Override + public int getByUid(Uid uid, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + SmartHRClient.JobTitle dept = client.getJobTitle(uid, options, fetchFieldsSet); + + if (dept != null) { + resultsHandler.handle(toConnectorObject(schema, dept, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getByName(Name name, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + SmartHRClient.JobTitle dept = client.getJobTitle(name, options, fetchFieldsSet); + + if (dept != null) { + resultsHandler.handle(toConnectorObject(schema, dept, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getAll(ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + return client.getJobTitles((jobTitle) -> resultsHandler.handle(toConnectorObject(schema, jobTitle, returnAttributesSet, allowPartialAttributeValues)), + options, fetchFieldsSet, pageSize, pageOffset); + } +} diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRObjectHandler.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRObjectHandler.java new file mode 100644 index 0000000..d4f05e7 --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRObjectHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.framework.common.objects.*; + +import java.util.Set; + +public interface SmartHRObjectHandler { + + Uid create(Set attributes); + + Set updateDelta(Uid uid, Set modifications, OperationOptions options); + + void delete(Uid uid, OperationOptions options); + + int getByUid(Uid uid, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset); + + int getByName(Name name, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset); + + int getAll(ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset); + + default ConnectorObject toConnectorObject(SchemaDefinition schema, T crew, + Set returnAttributesSet, boolean allowPartialAttributeValues) { + ConnectorObjectBuilder builder = schema.toConnectorObjectBuilder(crew, returnAttributesSet, allowPartialAttributeValues); + return builder.build(); + } + + SchemaDefinition getSchema(); + +} diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRQueryHandler.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRQueryHandler.java new file mode 100644 index 0000000..040503b --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRQueryHandler.java @@ -0,0 +1,21 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +@FunctionalInterface +public interface SmartHRQueryHandler { + boolean handle(T arg); +} \ No newline at end of file diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRSchema.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRSchema.java new file mode 100644 index 0000000..d283799 --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRSchema.java @@ -0,0 +1,88 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.framework.common.objects.ObjectClass; +import org.identityconnectors.framework.common.objects.OperationOptionInfoBuilder; +import org.identityconnectors.framework.common.objects.Schema; +import org.identityconnectors.framework.common.objects.SchemaBuilder; +import org.identityconnectors.framework.spi.operations.SearchOp; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * Schema for SmartHR objects. + * + * @author Hiroyuki Wada + */ +public class SmartHRSchema { + + private final SmartHRConfiguration configuration; + private final SmartHRClient client; + + public final Schema schema; + + private Map schemaHandlerMap; + + public SmartHRSchema(SmartHRConfiguration configuration, SmartHRClient client, + List smarthrSchema) { + this.configuration = configuration; + this.client = client; + this.schemaHandlerMap = new HashMap<>(); + + SchemaBuilder schemaBuilder = new SchemaBuilder(SmartHRConnector.class); + + buildSchema(schemaBuilder, SmartHRCrewHandler.createSchema(smarthrSchema).build(), + (schema) -> new SmartHRCrewHandler(configuration, client, schema)); + + buildSchema(schemaBuilder, SmartHRDepartmentHandler.createSchema().build(), + (schema) -> new SmartHRDepartmentHandler(configuration, client, schema)); + + buildSchema(schemaBuilder, SmartHREmploymentTypeHandler.createSchema().build(), + (schema) -> new SmartHREmploymentTypeHandler(configuration, client, schema)); + + buildSchema(schemaBuilder, SmartHRJobTitleHandler.createSchema().build(), + (schema) -> new SmartHRJobTitleHandler(configuration, client, schema)); + + buildSchema(schemaBuilder, SmartHRCompanyHandler.createSchema().build(), + (schema) -> new SmartHRCompanyHandler(configuration, client, schema)); + + buildSchema(schemaBuilder, SmartHRBizEstablishmentHandler.createSchema().build(), + (schema) -> new SmartHRBizEstablishmentHandler(configuration, client, schema)); + + // Define operation options + schemaBuilder.defineOperationOption(OperationOptionInfoBuilder.buildAttributesToGet(), SearchOp.class); + schemaBuilder.defineOperationOption(OperationOptionInfoBuilder.buildReturnDefaultAttributes(), SearchOp.class); + schemaBuilder.defineOperationOption(OperationOptionInfoBuilder.buildPageSize(), SearchOp.class); + schemaBuilder.defineOperationOption(OperationOptionInfoBuilder.buildPagedResultsOffset(), SearchOp.class); + + this.schema = schemaBuilder.build(); + + } + + private void buildSchema(SchemaBuilder builder, SchemaDefinition schemaDefinition, Function callback) { + builder.defineObjectClass(schemaDefinition.getObjectClassInfo()); + SmartHRObjectHandler handler = callback.apply(schemaDefinition); + this.schemaHandlerMap.put(schemaDefinition.getType(), handler); + } + + public SmartHRObjectHandler getSchemaHandler(ObjectClass objectClass) { + return schemaHandlerMap.get(objectClass.getObjectClassValue()); + } +} \ No newline at end of file diff --git a/src/main/java/jp/openstandia/connector/smarthr/SmartHRUtils.java b/src/main/java/jp/openstandia/connector/smarthr/SmartHRUtils.java new file mode 100644 index 0000000..0e0b989 --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/SmartHRUtils.java @@ -0,0 +1,122 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.objects.OperationOptions; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Provides utility methods + * + * @author Hiroyuki Wada + */ +public class SmartHRUtils { + private static final Log LOG = Log.getLog(SmartHRUtils.class); + + public static ZonedDateTime toZoneDateTime(String yyyymmdd) { + LocalDate date = LocalDate.parse(yyyymmdd); + return date.atStartOfDay(ZoneId.systemDefault()); + } + + /** + * Check if attrsToGetSet contains the attribute. + * + * @param attrsToGetSet + * @param attr + * @param isReturnByDefault + * @return + */ + public static boolean shouldReturn(Set attrsToGetSet, String attr, boolean isReturnByDefault) { + if (attrsToGetSet == null) { + return isReturnByDefault; + } + return attrsToGetSet.contains(attr); + } + + /** + * Check if ALLOW_PARTIAL_ATTRIBUTE_VALUES == true. + * + * @param options + * @return + */ + public static boolean shouldAllowPartialAttributeValues(OperationOptions options) { + // If the option isn't set from IDM, it may be null. + return Boolean.TRUE.equals(options.getAllowPartialAttributeValues()); + } + + /** + * Check if RETURN_DEFAULT_ATTRIBUTES == true. + * + * @param options + * @return + */ + public static boolean shouldReturnDefaultAttributes(OperationOptions options) { + // If the option isn't set from IDM, it may be null. + return Boolean.TRUE.equals(options.getReturnDefaultAttributes()); + } + + /** + * Create full map of ATTRIBUTES_TO_GET which is composed by RETURN_DEFAULT_ATTRIBUTES + ATTRIBUTES_TO_GET. + * Key: attribute name of the connector (e.g. __UID__) + * Value: field name for resource fetching + * + * @param schema + * @param options + * @return + */ + public static Map createFullAttributesToGet(SchemaDefinition schema, OperationOptions options) { + Map attributesToGet = new HashMap<>(); + attributesToGet.putAll(toReturnedByDefaultAttributesSet(schema)); + + if (options.getAttributesToGet() != null) { + for (String a : options.getAttributesToGet()) { + String fetchField = schema.getFetchField(a); + if (fetchField == null) { + LOG.warn("Requested unknown attribute to get. Ignored it: {0}", a); + continue; + } + attributesToGet.put(a, fetchField); + } + } + + return attributesToGet; + } + + private static Map toReturnedByDefaultAttributesSet(SchemaDefinition schema) { + return schema.getReturnedByDefaultAttributesSet(); + } + + public static int resolvePageSize(SmartHRConfiguration configuration, OperationOptions options) { + if (options.getPageSize() != null) { + return options.getPageSize(); + } + return configuration.getDefaultQueryPageSize(); + } + + public static int resolvePageOffset(OperationOptions options) { + if (options.getPagedResultsOffset() != null) { + return options.getPagedResultsOffset(); + } + return 0; + } +} diff --git a/src/main/java/jp/openstandia/connector/smarthr/rest/SmartHRRESTClient.java b/src/main/java/jp/openstandia/connector/smarthr/rest/SmartHRRESTClient.java new file mode 100644 index 0000000..cea6aca --- /dev/null +++ b/src/main/java/jp/openstandia/connector/smarthr/rest/SmartHRRESTClient.java @@ -0,0 +1,877 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr.rest; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jp.openstandia.connector.smarthr.SmartHRClient; +import jp.openstandia.connector.smarthr.SmartHRConfiguration; +import jp.openstandia.connector.smarthr.SmartHRQueryHandler; +import okhttp3.*; +import org.identityconnectors.common.StringUtil; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.*; +import org.identityconnectors.framework.common.objects.Name; +import org.identityconnectors.framework.common.objects.ObjectClass; +import org.identityconnectors.framework.common.objects.OperationOptions; +import org.identityconnectors.framework.common.objects.Uid; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import static jp.openstandia.connector.smarthr.SmartHRBizEstablishmentHandler.BIZ_ESTABLISHMENT_OBJECT_CLASS; +import static jp.openstandia.connector.smarthr.SmartHRCompanyHandler.COMPANY_OBJECT_CLASS; +import static jp.openstandia.connector.smarthr.SmartHRCrewHandler.CREW_OBJECT_CLASS; +import static jp.openstandia.connector.smarthr.SmartHRDepartmentHandler.DEPARTMENT_OBJECT_CLASS; +import static jp.openstandia.connector.smarthr.SmartHREmploymentTypeHandler.EMPLOYMENT_TYPE_OBJECT_CLASS; +import static jp.openstandia.connector.smarthr.SmartHRJobTitleHandler.JOB_TITLE_OBJECT_CLASS; + +public class SmartHRRESTClient implements SmartHRClient { + + private static final Log LOG = Log.getLog(SmartHRRESTClient.class); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final String instanceName; + private final SmartHRConfiguration configuration; + private final OkHttpClient httpClient; + + public SmartHRRESTClient(String instanceName, SmartHRConfiguration configuration, OkHttpClient httpClient) { + this.instanceName = instanceName; + this.configuration = configuration; + this.httpClient = httpClient; + } + + @Override + public void test() { + try (Response response = get(getCustomSchemaFieldEndpointURL(configuration))) { + if (response.code() != 200) { + // Something wrong.. + String body = response.body().string(); + throw new ConnectionFailedException(String.format("Unexpected authentication response. statusCode: %s, body: %s", + response.code(), + body)); + } + + LOG.info("[{0}] SmartHR connector's connection test is OK", instanceName); + + } catch (IOException e) { + throw new ConnectionFailedException("Cannot connect to SmartHR REST API", e); + } + } + + @Override + public List schema() { + try (Response response = get(getCustomSchemaFieldEndpointURL(configuration))) { + if (response.code() == 404) { + // Don't throw + return null; + } + + if (response.code() != 200) { + throw new ConnectorIOException(String.format("Failed to get SmartHR custom schema fields. statusCode: %d", response.code())); + } + + if (response.code() == 404) { + // Don't throw + return null; + } + + // Success + List fields = MAPPER.readValue(response.body().byteStream(), + new TypeReference>() { + }); + + return fields; + + } catch (IOException e) { + throw new ConnectorIOException("Failed to call SmartHR get schema API", e); + } + } + + @Override + public void close() { + } + + // Crew + + @Override + public Uid createCrew(Crew newCrew) throws AlreadyExistsException { + try (Response response = post(getCrewEndpointURL(configuration), newCrew)) { + if (response.code() == 400) { + ErrorResponse error = MAPPER.readValue(response.body().byteStream(), ErrorResponse.class); + if (error.isAlreadyExists()) { + throw new AlreadyExistsException(String.format("Crew '%s' already exists.", newCrew.emp_code)); + } + throw new InvalidAttributeValueException(String.format("Bad request when creating a crew. emp_code: %s", newCrew.emp_code)); + } + + if (response.code() != 201) { + throw new ConnectorIOException(String.format("Failed to create SmartHR crew: %s, statusCode: %d", newCrew.emp_code, response.code())); + } + + Crew created = MAPPER.readValue(response.body().byteStream(), Crew.class); + + // Created + if (created.emp_code != null) { + return new Uid(created.id, new Name(created.emp_code)); + } + // Use "id" as __NAME__ + return new Uid(created.id, new Name(created.id)); + + } catch (IOException e) { + throw new ConnectorIOException("Failed to call SmartHR create crew API", e); + } + } + + @Override + public Crew getCrew(Uid uid, OperationOptions options, Set fetchFieldsSet) { + try (Response response = get(getCrewEndpointURL(configuration, uid))) { + if (response.code() == 404) { + // Don't throw + return null; + } + + if (response.code() != 200) { + throw new ConnectorIOException(String.format("Failed to get SmartHR crew: %s, statusCode: %d", uid.getUidValue(), response.code())); + } + + Crew found = MAPPER.readValue(response.body().byteStream(), Crew.class); + + return found; + + } catch (IOException e) { + throw new ConnectorIOException("Failed to call SmartHR get crew API", e); + } + } + + @Override + public Crew getCrew(Name name, OperationOptions options, Set fetchFieldsSet) { + Map params = new HashMap<>(); + params.put("emp_code", name.getNameValue()); + + try (Response response = get(getCrewEndpointURL(configuration), params, 1, 1)) { + if (response.code() != 200) { + throw new ConnectorIOException(String.format("Failed to get SmartHR crew by emp_code. statusCode: %d", response.code())); + } + + // Success + List crews = MAPPER.readValue(response.body().byteStream(), + new TypeReference>() { + }); + if (crews.size() == 0) { + return null; + } + + return crews.get(0); + + } catch (IOException e) { + throw new ConnectorIOException("Failed to call SmartHR list crews API", e); + } + } + + @Override + public void updateCrew(Uid uid, Crew update) { + callPatch(CREW_OBJECT_CLASS, getCrewEndpointURL(configuration, uid), uid, update); + } + + @Override + public void deleteCrew(Uid uid, OperationOptions options) { + callDelete(CREW_OBJECT_CLASS, getCrewEndpointURL(configuration, uid), uid); + } + + @Override + public int getCrews(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset) { + // TODO Support sort by other attributes + Map params = new HashMap<>(); + params.put("sort", "emp_code"); + params.put("fields", String.join(",", fetchFieldsSet)); + + return getAll(handler, options, params, pageSize, pageOffset, getCrewEndpointURL(configuration), new TypeReference>() { + }, CREW_OBJECT_CLASS); + } + + // Department + + @Override + public Uid createDepartment(Department newDept) throws AlreadyExistsException { + try (Response response = post(getDeptEndpointURL(configuration), newDept)) { + if (response.code() == 400) { + ErrorResponse error = MAPPER.readValue(response.body().byteStream(), ErrorResponse.class); + if (error.isAlreadyExists()) { + throw new AlreadyExistsException(String.format("Department '%s' already exists.", newDept.code)); + } + throw new InvalidAttributeValueException(String.format("Bad request when creating a department. emp_code: %s", newDept.code)); + } + + if (response.code() != 201) { + throw new ConnectorIOException(String.format("Failed to create SmartHR department: %s, statusCode: %d", newDept.code, response.code())); + } + + Department created = MAPPER.readValue(response.body().byteStream(), Department.class); + + // Created + if (created.code != null) { + return new Uid(created.id, new Name(created.code)); + } + // Use "id" as __NAME__ + return new Uid(created.id, new Name(created.id)); + + } catch (IOException e) { + throw new ConnectorIOException("Failed to call SmartHR create department API", e); + } + } + + @Override + public Department getDepartment(Uid uid, OperationOptions options, Set fetchFieldsSet) { + try (Response response = get(getDeptEndpointURL(configuration, uid))) { + if (response.code() == 404) { + // Don't throw + return null; + } + + if (response.code() != 200) { + throw new ConnectorIOException(String.format("Failed to get SmartHR department: %s, statusCode: %d", uid.getUidValue(), response.code())); + } + + Department found = MAPPER.readValue(response.body().byteStream(), Department.class); + + return found; + + } catch (IOException e) { + throw new ConnectorIOException("Failed to call SmartHR get department API", e); + } + } + + @Override + public Department getDepartment(Name name, OperationOptions options, Set fetchFieldsSet) { + Map params = new HashMap<>(); + params.put("code", name.getNameValue()); + + try (Response response = get(getDeptEndpointURL(configuration), params, 1, 1)) { + if (response.code() != 200) { + throw new ConnectorIOException(String.format("Failed to get SmartHR department by code. statusCode: %d", response.code())); + } + + // Success + List dept = MAPPER.readValue(response.body().byteStream(), + new TypeReference>() { + }); + if (dept.size() == 0) { + return null; + } + + return dept.get(0); + + } catch (IOException e) { + throw new ConnectorIOException("Failed to call SmartHR list depts API", e); + } + } + + @Override + public void updateDepartment(Uid uid, Department update) { + callPatch(DEPARTMENT_OBJECT_CLASS, getDeptEndpointURL(configuration, uid), uid, update); + } + + @Override + public void deleteDepartment(Uid uid, OperationOptions options) { + callDelete(DEPARTMENT_OBJECT_CLASS, getDeptEndpointURL(configuration, uid), uid); + } + + @Override + public int getDepartments(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset) { + // TODO Support sort by other attributes + Map params = new HashMap<>(); + params.put("sort", "code"); + + return getAll(handler, options, params, pageSize, pageOffset, getDeptEndpointURL(configuration), new TypeReference>() { + }, DEPARTMENT_OBJECT_CLASS); + } + + // EmploymentType + + @Override + public Uid createEmploymentType(EmploymentType newEmpType) throws AlreadyExistsException { + try (Response response = post(getEmpTypeEndpointURL(configuration), newEmpType)) { + if (response.code() == 400) { + ErrorResponse error = MAPPER.readValue(response.body().byteStream(), ErrorResponse.class); + if (error.isAlreadyExists()) { + throw new AlreadyExistsException(String.format("Department '%s' already exists.", newEmpType.name)); + } + throw new InvalidAttributeValueException(String.format("Bad request when creating an employment_type. emp_code: %s", newEmpType.name)); + } + + if (response.code() != 201) { + throw new ConnectorIOException(String.format("Failed to create SmartHR employment_type: %s, statusCode: %d", newEmpType.name, response.code())); + } + + EmploymentType created = MAPPER.readValue(response.body().byteStream(), EmploymentType.class); + + // Created + if (created.name != null) { + return new Uid(created.id, new Name(created.name)); + } + // Use "id" as __NAME__ + return new Uid(created.id, new Name(created.id)); + + } catch (IOException e) { + throw new ConnectorIOException("Failed to call SmartHR create employment_type API", e); + } + } + + @Override + public EmploymentType getEmploymentType(Uid uid, OperationOptions options, Set fetchFieldsSet) { + try (Response response = get(getEmpTypeEndpointURL(configuration, uid))) { + if (response.code() == 404) { + // Don't throw + return null; + } + + if (response.code() != 200) { + throw new ConnectorIOException(String.format("Failed to get SmartHR employment_type: %s, statusCode: %d", uid.getUidValue(), response.code())); + } + + EmploymentType found = MAPPER.readValue(response.body().byteStream(), EmploymentType.class); + + return found; + + } catch (IOException e) { + throw new ConnectorIOException("Failed to call SmartHR get employment_type API", e); + } + } + + @Override + public EmploymentType getEmploymentType(Name name, OperationOptions options, Set fetchFieldsSet) { + // No API to fetch by name currently. + // We need to fetch all employment_type and filter them by name. + final AtomicReference result = new AtomicReference<>(); + getEmploymentTypes(empType -> { + // Case sensitive + if (empType.name.equals(name.getNameValue())) { + result.set(empType); + return false; + } + return true; + }, options, fetchFieldsSet, configuration.getDefaultQueryPageSize(), 0); + + return result.get(); + } + + @Override + public void updateEmploymentType(Uid uid, EmploymentType update) { + callPatch(EMPLOYMENT_TYPE_OBJECT_CLASS, getEmpTypeEndpointURL(configuration, uid), uid, update); + } + + @Override + public void deleteEmploymentType(Uid uid, OperationOptions options) { + callDelete(EMPLOYMENT_TYPE_OBJECT_CLASS, getEmpTypeEndpointURL(configuration, uid), uid); + } + + @Override + public int getEmploymentTypes(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset) { + return getAll(handler, options, pageSize, pageOffset, getEmpTypeEndpointURL(configuration), new TypeReference>() { + }, EMPLOYMENT_TYPE_OBJECT_CLASS); + } + + // JobTitle + + @Override + public Uid createJobTitle(JobTitle newJobTitle) throws AlreadyExistsException { + try (Response response = post(getJobTitleEndpointURL(configuration), newJobTitle)) { + if (response.code() == 400) { + ErrorResponse error = MAPPER.readValue(response.body().byteStream(), ErrorResponse.class); + if (error.isAlreadyExists()) { + throw new AlreadyExistsException(String.format("Department '%s' already exists.", newJobTitle.name)); + } + throw new InvalidAttributeValueException(String.format("Bad request when creating an job_title. emp_code: %s", newJobTitle.name)); + } + + if (response.code() != 201) { + throw new ConnectorIOException(String.format("Failed to create SmartHR job_title: %s, statusCode: %d", newJobTitle.name, response.code())); + } + + JobTitle created = MAPPER.readValue(response.body().byteStream(), JobTitle.class); + + // Created + if (created.name != null) { + return new Uid(created.id, new Name(created.name)); + } + // Use "id" as __NAME__ + return new Uid(created.id, new Name(created.id)); + + } catch (IOException e) { + throw new ConnectorIOException("Failed to call SmartHR create job_title API", e); + } + } + + @Override + public JobTitle getJobTitle(Uid uid, OperationOptions options, Set fetchFieldsSet) { + try (Response response = get(getJobTitleEndpointURL(configuration, uid))) { + if (response.code() == 404) { + // Don't throw + return null; + } + + if (response.code() != 200) { + throw new ConnectorIOException(String.format("Failed to get SmartHR job_title: %s, statusCode: %d", uid.getUidValue(), response.code())); + } + + JobTitle found = MAPPER.readValue(response.body().byteStream(), JobTitle.class); + + return found; + + } catch (IOException e) { + throw new ConnectorIOException("Failed to call SmartHR get job_title API", e); + } + } + + @Override + public JobTitle getJobTitle(Name name, OperationOptions options, Set fetchFieldsSet) { + // No API to fetch by name currently. + // We need to fetch all job titles and filter them by name. + final AtomicReference result = new AtomicReference<>(); + getJobTitles(j -> { + // Case sensitive + if (j.name.equals(name.getNameValue())) { + result.set(j); + return false; + } + return true; + }, options, fetchFieldsSet, configuration.getDefaultQueryPageSize(), 0); + + return result.get(); + } + + @Override + public void updateJobTitle(Uid uid, JobTitle update) { + callPatch(JOB_TITLE_OBJECT_CLASS, getJobTitleEndpointURL(configuration, uid), uid, update); + } + + @Override + public void deleteJobTitle(Uid uid, OperationOptions options) { + callDelete(JOB_TITLE_OBJECT_CLASS, getJobTitleEndpointURL(configuration, uid), uid); + } + + @Override + public int getJobTitles(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset) { + return getAll(handler, options, pageSize, pageOffset, getJobTitleEndpointURL(configuration), new TypeReference>() { + }, JOB_TITLE_OBJECT_CLASS); + } + + // Company + + @Override + public Company getCompany(Uid uid, OperationOptions options, Set fetchFieldsSet) { + // No API to fetch by uid currently. + // We need to fetch all companies and filter them by name. + final AtomicReference result = new AtomicReference<>(); + getCompanies(c -> { + // Case in-sensitive + if (c.id.equalsIgnoreCase(uid.getUidValue())) { + result.set(c); + return false; + } + return true; + }, options, fetchFieldsSet, configuration.getDefaultQueryPageSize(), 0); + + return result.get(); + } + + @Override + public Company getCompany(Name name, OperationOptions options, Set fetchFieldsSet) { + // No API to fetch by name currently. + // We need to fetch all companies and filter them by name. + final AtomicReference result = new AtomicReference<>(); + getCompanies(c -> { + // Case sensitive + if (c.name.equals(name.getNameValue())) { + result.set(c); + return false; + } + return true; + }, options, fetchFieldsSet, configuration.getDefaultQueryPageSize(), 0); + + return result.get(); + } + + @Override + public int getCompanies(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset) { + return getAll(handler, options, pageSize, pageOffset, getCompanyEndpointURL(configuration), new TypeReference>() { + }, COMPANY_OBJECT_CLASS); + } + + // Biz Establishment + + @Override + public BizEstablishment getBizEstablishment(Uid uid, OperationOptions options, Set fetchFieldsSet) { + // No API to fetch by uid currently. + // We need to fetch all biz_establishments and filter them by name. + final AtomicReference result = new AtomicReference<>(); + getBizEstablishments(b -> { + // Case in-sensitive + if (b.id.equalsIgnoreCase(uid.getUidValue())) { + result.set(b); + return false; + } + return true; + }, options, fetchFieldsSet, configuration.getDefaultQueryPageSize(), 0); + + return result.get(); + } + + @Override + public BizEstablishment getBizEstablishment(Name name, OperationOptions options, Set fetchFieldsSet) { + // No API to fetch by name currently. + // We need to fetch all biz_establishments and filter them by name. + final AtomicReference result = new AtomicReference<>(); + getBizEstablishments(b -> { + // Case sensitive + if (b.name.equals(name.getNameValue())) { + result.set(b); + return false; + } + return true; + }, options, fetchFieldsSet, configuration.getDefaultQueryPageSize(), 0); + + return result.get(); + } + + @Override + public int getBizEstablishments(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset) { + return getAll(handler, options, pageSize, pageOffset, getBizEstablishmentEndpointURL(configuration), new TypeReference>() { + }, BIZ_ESTABLISHMENT_OBJECT_CLASS); + } + + // Utilities + + protected void callPatch(ObjectClass objectClass, String url, Uid uid, Object target) { + try (Response response = patch(url, target)) { + if (response.code() == 400) { + throw new InvalidAttributeValueException(String.format("Bad request when updating %s: %s, response: %s", + objectClass.getObjectClassValue(), uid.getUidValue(), toBody(response))); + } + + if (response.code() == 404) { + throw new UnknownUidException(uid, objectClass); + } + + if (response.code() != 200) { + throw new ConnectorIOException(String.format("Failed to patch SmartHR %s: %s, statusCode: %d, response: %s", + objectClass.getObjectClassValue(), uid.getUidValue(), response.code(), toBody(response))); + } + + // Success + + } catch (IOException e) { + throw new ConnectorIOException(String.format("Failed to patch SmartHR %s: %s", + objectClass.getObjectClassValue(), uid.getUidValue()), e); + } + } + + protected void callUpdate(ObjectClass objectClass, String url, Uid uid, Object target) { + try (Response response = put(url, target)) { + if (response.code() == 400) { + throw new InvalidAttributeValueException(String.format("Bad request when updating %s: %s, response: %s", + objectClass.getObjectClassValue(), uid.getUidValue(), toBody(response))); + } + + if (response.code() == 404) { + throw new UnknownUidException(uid, objectClass); + } + + if (response.code() != 200) { + throw new ConnectorIOException(String.format("Failed to update SmartHR %s: %s, statusCode: %d, response: %s", + objectClass.getObjectClassValue(), uid.getUidValue(), response.code(), toBody(response))); + } + + // Success + + } catch (IOException e) { + throw new ConnectorIOException(String.format("Failed to update SmartHR %s: %s", + objectClass.getObjectClassValue(), uid.getUidValue()), e); + } + } + + private String toBody(Response response) { + ResponseBody resBody = response.body(); + if (resBody == null) { + return null; + } + try { + return resBody.string(); + } catch (IOException e) { + LOG.error(e, "Unexpected smarthr API response"); + return ""; + } + } + + /** + * Generic delete method. + * + * @param objectClass + * @param url + * @param uid + */ + protected void callDelete(ObjectClass objectClass, String url, Uid uid) { + try (Response response = delete(url)) { + if (response.code() == 404) { + throw new UnknownUidException(uid, objectClass); + } + + if (response.code() != 204) { + throw new ConnectorIOException(String.format("Failed to delete smarthr %s: %s, statusCode: %d, response: %s", + objectClass.getObjectClassValue(), uid.getUidValue(), response.code(), toBody(response))); + } + + // Success + + } catch (IOException e) { + throw new ConnectorIOException(String.format("Failed to delete smarthr %s: %s", + objectClass.getObjectClassValue(), uid.getUidValue()), e); + } + } + + private RequestBody createJsonRequestBody(Object body) { + String bodyString; + try { + bodyString = MAPPER.writeValueAsString(body); + } catch (JsonProcessingException e) { + throw new ConnectorIOException("Failed to write request json body", e); + } + + return RequestBody.create(bodyString, MediaType.parse("application/json; charset=UTF-8")); + } + + private void throwExceptionIfUnauthorized(Response response) throws ConnectorIOException { + if (response.code() == 401) { + throw new ConnectionFailedException("Cannot authenticate to the SmartHR REST API: " + response.message()); + } + } + + private void throwExceptionIfServerError(Response response) throws ConnectorIOException { + if (response.code() >= 500 && response.code() <= 599) { + try { + String body = response.body().string(); + throw new ConnectorIOException("SmartHR server error: " + body); + } catch (IOException e) { + throw new ConnectorIOException("SmartHR server error", e); + } + } + } + + private Response get(String url) throws IOException { + return get(url, null, -1, -1); + } + + private Response get(String url, int page, int pageSize) throws IOException { + return get(url, null, page, pageSize); + } + + private Response get(String url, Map params, int page, int pageSize) throws IOException { + HttpUrl.Builder httpBuilder = HttpUrl.parse(url).newBuilder(); + if (page != -1) { + httpBuilder.addQueryParameter("page", String.valueOf(page)); + } + if (pageSize != 1) { + httpBuilder.addQueryParameter("per_page", String.valueOf(pageSize)); + } + if (params != null) { + params.entrySet().stream().forEach(entry -> httpBuilder.addQueryParameter(entry.getKey(), entry.getValue())); + } + + final Request request = new Request.Builder() + .url(httpBuilder.build()) + .get() + .build(); + + final Response response = httpClient.newCall(request).execute(); + + throwExceptionIfUnauthorized(response); + throwExceptionIfServerError(response); + + return response; + } + + protected int getAll(SmartHRQueryHandler handler, OperationOptions options, int pageSize, int pageOffset, + String endpointURL, TypeReference> valueTypeRef, ObjectClass objectClass) { + return getAll(handler, options, null, pageSize, pageOffset, endpointURL, valueTypeRef, objectClass); + } + + protected int getAll(SmartHRQueryHandler handler, OperationOptions options, Map params, int pageSize, int pageOffset, + String endpointURL, TypeReference> valueTypeRef, ObjectClass objectClass) { + // Start from 1 in SmartHR + int page = 1; + + // If pageOffset is 1, it means showing first page only + if (pageOffset > 1) { + page = (int) Math.ceil(pageOffset / pageSize) + 1; + } + + int totalCount; + + // If no requested pageOffset, fetch all pages + while (true) { + try (Response response = get(endpointURL, params, page, pageSize)) { + if (response.code() != 200) { + ErrorResponse error = MAPPER.readValue(response.body().byteStream(), ErrorResponse.class); + throw new ConnectorIOException(String.format("Failed to get SmartHR %s. statusCode: %d, message: %s", + objectClass.getObjectClassValue(), response.code(), response.message())); + } + + // Success + totalCount = getTotalCount(response); + + List objects = MAPPER.readValue(response.body().byteStream(), valueTypeRef); + if (objects.size() == 0) { + break; + } + + for (T object : objects) { + if (!handler.handle(object)) { + break; + } + } + + if (pageOffset > 0) { + // If requested pageOffset, don't process paging + break; + } + + page = getPage(response); + pageSize = getPerPage(response); + + if ((page * pageSize) < totalCount) { + page++; + continue; + } + + break; + + } catch (IOException e) { + throw new ConnectorIOException(String.format("Failed to call SmartHR list %s API", objectClass.getObjectClassValue()), e); + } + } + + return totalCount; + } + + private int getPage(Response response) { + String value = response.header("x-page"); + if (StringUtil.isNotEmpty(value)) { + return Integer.parseInt(value); + } + return -1; + } + + private int getPerPage(Response response) { + String value = response.header("x-per-page"); + if (StringUtil.isNotEmpty(value)) { + return Integer.parseInt(value); + } + return -1; + } + + private int getTotalCount(Response response) { + String value = response.header("x-total-count"); + if (StringUtil.isNotEmpty(value)) { + return Integer.parseInt(value); + } + return -1; + } + + private Response post(String url, Object body) throws IOException { + RequestBody requestBody = createJsonRequestBody(body); + + for (int i = 0; i < 2; i++) { + final Request request = new Request.Builder() + .url(url) + .post(requestBody) + .build(); + + final Response response = httpClient.newCall(request).execute(); + + throwExceptionIfUnauthorized(response); + throwExceptionIfServerError(response); + + return response; + } + + throw new ConnectorIOException("Failed to call post API"); + } + + private Response put(String url, Object body) throws IOException { + RequestBody requestBody = createJsonRequestBody(body); + + for (int i = 0; i < 2; i++) { + final Request request = new Request.Builder() + .url(url) + .put(requestBody) + .build(); + + final Response response = httpClient.newCall(request).execute(); + + throwExceptionIfUnauthorized(response); + throwExceptionIfServerError(response); + + return response; + } + + throw new ConnectorIOException("Failed to call post API"); + } + + private Response patch(String url, Object body) throws IOException { + RequestBody requestBody = createJsonRequestBody(body); + + for (int i = 0; i < 2; i++) { + final Request request = new Request.Builder() + .url(url) + .patch(requestBody) + .build(); + + final Response response = httpClient.newCall(request).execute(); + + throwExceptionIfUnauthorized(response); + throwExceptionIfServerError(response); + + return response; + } + + throw new ConnectorIOException("Failed to call patch API"); + } + + private Response delete(String url) throws IOException { + for (int i = 0; i < 2; i++) { + final Request request = new Request.Builder() + .url(url) + .delete() + .build(); + + final Response response = httpClient.newCall(request).execute(); + + throwExceptionIfUnauthorized(response); + throwExceptionIfServerError(response); + + return response; + } + + throw new ConnectorIOException("Failed to call delete API"); + } +} diff --git a/src/test/java/jp/openstandia/connector/smarthr/SchemaTest.java b/src/test/java/jp/openstandia/connector/smarthr/SchemaTest.java new file mode 100644 index 0000000..83bd95d --- /dev/null +++ b/src/test/java/jp/openstandia/connector/smarthr/SchemaTest.java @@ -0,0 +1,50 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import jp.openstandia.connector.smarthr.testutil.AbstractTest; +import org.identityconnectors.framework.common.objects.ObjectClassInfo; +import org.identityconnectors.framework.common.objects.Schema; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class SchemaTest extends AbstractTest { + + @Test + void schema() { + Schema schema = connector.schema(); + + assertNotNull(schema); + assertEquals(6, schema.getObjectClassInfo().size()); + + Optional crew = schema.getObjectClassInfo().stream().filter(o -> o.is("crew")).findFirst(); + Optional department = schema.getObjectClassInfo().stream().filter(o -> o.is("department")).findFirst(); + Optional employmentType = schema.getObjectClassInfo().stream().filter(o -> o.is("employment_type")).findFirst(); + Optional jobTitle = schema.getObjectClassInfo().stream().filter(o -> o.is("job_title")).findFirst(); + Optional company = schema.getObjectClassInfo().stream().filter(o -> o.is("company")).findFirst(); + Optional bizEstablishment = schema.getObjectClassInfo().stream().filter(o -> o.is("biz_establishment")).findFirst(); + + assertTrue(crew.isPresent()); + assertTrue(department.isPresent()); + assertTrue(employmentType.isPresent()); + assertTrue(jobTitle.isPresent()); + assertTrue(company.isPresent()); + assertTrue(bizEstablishment.isPresent()); + } +} diff --git a/src/test/java/jp/openstandia/connector/smarthr/SmartHRUtilsTest.java b/src/test/java/jp/openstandia/connector/smarthr/SmartHRUtilsTest.java new file mode 100644 index 0000000..b778de2 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/smarthr/SmartHRUtilsTest.java @@ -0,0 +1,38 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import org.identityconnectors.framework.common.objects.OperationOptions; +import org.identityconnectors.framework.common.objects.OperationOptionsBuilder; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SmartHRUtilsTest { + + @Test + void shouldReturnPartialAttributeValues() { + OperationOptions noOptions = new OperationOptionsBuilder().build(); + assertFalse(SmartHRUtils.shouldAllowPartialAttributeValues(noOptions)); + + OperationOptions falseOption = new OperationOptionsBuilder().setAllowPartialAttributeValues(false).build(); + assertFalse(SmartHRUtils.shouldAllowPartialAttributeValues(falseOption)); + + OperationOptions trueOption = new OperationOptionsBuilder().setAllowPartialAttributeValues(true).build(); + assertTrue(SmartHRUtils.shouldAllowPartialAttributeValues(trueOption)); + } +} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/smarthr/TestTest.java b/src/test/java/jp/openstandia/connector/smarthr/TestTest.java new file mode 100644 index 0000000..a14c950 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/smarthr/TestTest.java @@ -0,0 +1,27 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr; + +import jp.openstandia.connector.smarthr.testutil.AbstractTest; +import org.junit.jupiter.api.Test; + +class TestTest extends AbstractTest { + + @Test + void test() { + connector.test(); + } +} diff --git a/src/test/java/jp/openstandia/connector/smarthr/testutil/AbstractTest.java b/src/test/java/jp/openstandia/connector/smarthr/testutil/AbstractTest.java new file mode 100644 index 0000000..6b0d2f7 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/smarthr/testutil/AbstractTest.java @@ -0,0 +1,53 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr.testutil; + +import jp.openstandia.connector.smarthr.SmartHRConfiguration; +import org.identityconnectors.common.security.GuardedString; +import org.identityconnectors.framework.api.APIConfiguration; +import org.identityconnectors.framework.api.ConnectorFacade; +import org.identityconnectors.framework.api.ConnectorFacadeFactory; +import org.identityconnectors.test.common.TestHelpers; +import org.junit.jupiter.api.BeforeEach; + +public abstract class AbstractTest { + + protected ConnectorFacade connector; + protected MockClient mockClient; + + protected SmartHRConfiguration newConfiguration() { + SmartHRConfiguration conf = new SmartHRConfiguration(); + conf.setEndpointURL("http://localhost:8080/smarthr/api"); + conf.setApiAccessToken(new GuardedString("dummy".toCharArray())); + return conf; + } + + protected ConnectorFacade newFacade() { + ConnectorFacadeFactory factory = ConnectorFacadeFactory.getInstance(); + APIConfiguration impl = TestHelpers.createTestConfiguration(LocalSmartHRConnector.class, newConfiguration()); + impl.getResultsHandlerConfiguration().setEnableAttributesToGetSearchResultsHandler(false); + impl.getResultsHandlerConfiguration().setEnableNormalizingResultsHandler(false); + impl.getResultsHandlerConfiguration().setEnableFilteredResultsHandler(false); + return factory.newInstance(impl); + } + + @BeforeEach + void before() { + connector = newFacade(); + mockClient = MockClient.instance(); + mockClient.init(); + } +} diff --git a/src/test/java/jp/openstandia/connector/smarthr/testutil/LocalSmartHRConnector.java b/src/test/java/jp/openstandia/connector/smarthr/testutil/LocalSmartHRConnector.java new file mode 100644 index 0000000..a3de3a5 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/smarthr/testutil/LocalSmartHRConnector.java @@ -0,0 +1,25 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr.testutil; + +import jp.openstandia.connector.smarthr.SmartHRConnector; + +public class LocalSmartHRConnector extends SmartHRConnector { + @Override + protected void authenticateResource() { + client = MockClient.instance(); + } +} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/smarthr/testutil/MockClient.java b/src/test/java/jp/openstandia/connector/smarthr/testutil/MockClient.java new file mode 100644 index 0000000..ae98380 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/smarthr/testutil/MockClient.java @@ -0,0 +1,212 @@ +/* + * Copyright Nomura Research Institute, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jp.openstandia.connector.smarthr.testutil; + +import jp.openstandia.connector.smarthr.SmartHRClient; +import jp.openstandia.connector.smarthr.SmartHRQueryHandler; +import org.identityconnectors.framework.common.exceptions.AlreadyExistsException; +import org.identityconnectors.framework.common.objects.Name; +import org.identityconnectors.framework.common.objects.OperationOptions; +import org.identityconnectors.framework.common.objects.Uid; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +public class MockClient implements SmartHRClient { + + private static final MockClient INSTANCE = new MockClient(); + + public boolean closed = false; + + public void init() { + closed = false; + } + + private MockClient() { + } + + public static MockClient instance() { + return INSTANCE; + } + + @Override + public void test() { + + } + + @Override + public List schema() { + return Collections.emptyList(); + } + + @Override + public void close() { + + } + + @Override + public Uid createCrew(Crew newCrew) throws AlreadyExistsException { + return null; + } + + @Override + public Crew getCrew(Uid uid, OperationOptions options, Set attributesToGet) { + return null; + } + + @Override + public Crew getCrew(Name name, OperationOptions options, Set attributesToGet) { + return null; + } + + @Override + public void updateCrew(Uid uid, Crew update) { + + } + + @Override + public void deleteCrew(Uid uid, OperationOptions options) { + + } + + @Override + public int getCrews(SmartHRQueryHandler handler, OperationOptions options, Set attributesToGet, int pageSize, int pageOffset) { + + return pageSize; + } + + @Override + public Uid createDepartment(Department newCrew) throws AlreadyExistsException { + return null; + } + + @Override + public Department getDepartment(Uid uid, OperationOptions options, Set attributesToGet) { + return null; + } + + @Override + public Department getDepartment(Name name, OperationOptions options, Set attributesToGet) { + return null; + } + + @Override + public void updateDepartment(Uid uid, Department update) { + + } + + @Override + public void deleteDepartment(Uid uid, OperationOptions options) { + + } + + @Override + public int getDepartments(SmartHRQueryHandler handler, OperationOptions options, Set attributesToGet, int pageSize, int pageOffset) { + + return pageSize; + } + + @Override + public Uid createEmploymentType(EmploymentType newCrew) throws AlreadyExistsException { + return null; + } + + @Override + public EmploymentType getEmploymentType(Uid uid, OperationOptions options, Set attributesToGet) { + return null; + } + + @Override + public EmploymentType getEmploymentType(Name name, OperationOptions options, Set attributesToGet) { + return null; + } + + @Override + public void updateEmploymentType(Uid uid, EmploymentType update) { + + } + + @Override + public void deleteEmploymentType(Uid uid, OperationOptions options) { + + } + + @Override + public int getEmploymentTypes(SmartHRQueryHandler handler, OperationOptions options, Set attributesToGet, int pageSize, int pageOffset) { + return 0; + } + + @Override + public Uid createJobTitle(JobTitle newCrew) throws AlreadyExistsException { + return null; + } + + @Override + public JobTitle getJobTitle(Uid uid, OperationOptions options, Set attributesToGet) { + return null; + } + + @Override + public JobTitle getJobTitle(Name name, OperationOptions options, Set attributesToGet) { + return null; + } + + @Override + public void updateJobTitle(Uid uid, JobTitle update) { + + } + + @Override + public void deleteJobTitle(Uid uid, OperationOptions options) { + + } + + @Override + public int getJobTitles(SmartHRQueryHandler handler, OperationOptions options, Set attributesToGet, int pageSize, int pageOffset) { + return 0; + } + + @Override + public Company getCompany(Uid uid, OperationOptions options, Set fetchFieldsSet) { + return null; + } + + @Override + public Company getCompany(Name name, OperationOptions options, Set fetchFieldsSet) { + return null; + } + + @Override + public int getCompanies(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset) { + return 0; + } + + @Override + public BizEstablishment getBizEstablishment(Uid uid, OperationOptions options, Set fetchFieldsSet) { + return null; + } + + @Override + public BizEstablishment getBizEstablishment(Name name, OperationOptions options, Set fetchFieldsSet) { + return null; + } + + @Override + public int getBizEstablishments(SmartHRQueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset) { + return 0; + } +}