Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>3.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.focus_shift.lexoffice.java.sdk;

import com.google.common.base.Preconditions;
import lombok.AccessLevel;
import lombok.Getter;

Expand All @@ -8,6 +9,7 @@ public class LexofficeApiBuilder {

public static final String LEXOFFICE_API = "api.lexware.io/v1";

private String protocol = "https";
private String host = LEXOFFICE_API;
private String apiToken = null;
private ThrottleProvider throttleProvider;
Expand All @@ -18,6 +20,15 @@ public LexofficeApiBuilder apiToken(String apiToken) {
}


public LexofficeApiBuilder protocol(String protocol) {
Preconditions.checkArgument(
"http".equals(protocol) || "https".equals(protocol),
"Protocol must be 'http' or 'https'"
);
this.protocol = protocol;
return this;
}

public LexofficeApiBuilder host(String host) {
this.host = host;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ protected JsonMapper getJsonMapper() {

public RestUriBuilder getUriBuilder() {
return new RestUriBuilder()
.protocol("https")
.protocol(apiBuilder.getProtocol())
.host(apiBuilder.getHost());
}

Expand Down
64 changes: 21 additions & 43 deletions src/main/java/de/focus_shift/lexoffice/java/sdk/RestUriBuilder.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
package de.focus_shift.lexoffice.java.sdk;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class RestUriBuilder {

private String host;
private String protocol = "https";
private StringBuilder path;
private Map<String, List<String>> parameters = new HashMap<>();
private String host;
private String path = "";
private final UriComponentsBuilder queryParamsBuilder = UriComponentsBuilder.newInstance();

public RestUriBuilder host(String host) {
Preconditions.checkNotNull(host);
Expand All @@ -33,55 +28,38 @@ public RestUriBuilder protocol(String protocol) {

public RestUriBuilder path(String path) {
Preconditions.checkNotNull(path);
this.path = new StringBuilder();
this.path.append(path);
this.path = path;
return this;
}

public RestUriBuilder appendPath(String path) {
Preconditions.checkNotNull(path);
this.path.append(path);
this.path = this.path + path;
return this;
}

public RestUriBuilder addParameter(String key, Object value) {
return addParameters(key, Collections.singletonList(value));
}

public RestUriBuilder addParameters(String key, Collection<?> value) {
public RestUriBuilder addParameters(String key, Collection<?> values) {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(value);
Iterable<String> stringValues = Iterables.transform(value, new Function<Object, String>() {
public String apply(Object o) {
return String.valueOf(o);
}
});
this.parameters.put(key, Lists.newArrayList(stringValues));
Preconditions.checkNotNull(values);
for (Object value : values) {
queryParamsBuilder.queryParam(key, value);
}
return this;
}

public String build() {
StringBuilder builder = new StringBuilder();
builder.append(protocol);
builder.append("://");
public URI build() {
Preconditions.checkNotNull(host);
Preconditions.checkNotNull(path);
builder.append(host);
builder.append(path);
if (!parameters.isEmpty()) {
builder.append("?");
Joiner.on("&")
.appendTo(builder, Iterables.transform(parameters.entrySet(), new Function<Map.Entry<String, List<String>>, String>() {
public String apply(final Map.Entry<String, List<String>> stringStringEntry) {
return Joiner.on("&")
.join(Lists.transform(stringStringEntry.getValue(), new Function<String, String>() {
public String apply(String s) {
return stringStringEntry.getKey() + "=" + s;
}
}));
}
}));
}
return builder.toString();

String baseUrl = protocol + "://" + host + path;

return UriComponentsBuilder.fromUriString(baseUrl)
.queryParams(queryParamsBuilder.build().getQueryParams())
.build()
.encode()
.toUri();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package de.focus_shift.lexoffice.java.sdk;

/**
* Utility class for encoding search strings according to Lexoffice API requirements.
*
* <p>Due to technical constraints, for some endpoints (contact, vouchers, voucherlist),
* search strings require special handling: HTML special characters {@code &}, {@code <},
* and {@code >} need to be sent in their HTML encoded form before URL encoding is applied.
*
* <p>The HTML encoding needs to match the canonical representation as stored in the
* Lexware database. Unicode character encoding or any other form of representation
* should not be used.
*
* @see <a href="https://developers.lexware.io/partner/docs/#faq-search-string-encoding">
* Lexoffice API - Search String Encoding</a>
*/
public final class SearchStringEncoder {

private SearchStringEncoder() {
// utility class
}

/**
* HTML encodes special characters in search strings for Lexoffice API endpoints
* that require this encoding (contact, vouchers, voucherlist).
*
* <p>The following characters are encoded:
* <ul>
* <li>{@code &} → {@code &amp;}</li>
* <li>{@code <} → {@code &lt;}</li>
* <li>{@code >} → {@code &gt;}</li>
* </ul>
*
* <p>Example: {@code "johnson & partner"} becomes {@code "johnson &amp; partner"}
*
* @param value the search string to encode, may be null
* @return the HTML encoded string, or null if input was null
* @see <a href="https://developers.lexware.io/partner/docs/#faq-search-string-encoding">
* Lexoffice API - Search String Encoding</a>
*/
public static String htmlEncode(String value) {
if (value == null) {
return null;
}
// Order matters: & must be encoded first to avoid double-encoding
return value
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


import de.focus_shift.lexoffice.java.sdk.RequestContext;
import de.focus_shift.lexoffice.java.sdk.SearchStringEncoder;
import de.focus_shift.lexoffice.java.sdk.model.Contact;
import de.focus_shift.lexoffice.java.sdk.model.ItemCreatedResult;
import de.focus_shift.lexoffice.java.sdk.model.Page;
Expand Down Expand Up @@ -68,19 +69,23 @@ public Fetch pageSize(int pageSize) {

/**
* filters contacts where any of their email addresses inside the emailAddresses JSON object match the given email value. At least 3 characters are necessary to successfully complete the query.
*
* @see SearchStringEncoder
*/
public Fetch email(String email) {
super.getUriBuilder()
.addParameter("email", email);
.addParameter("email", SearchStringEncoder.htmlEncode(email));
return this;
}

/**
* filters contacts whose name matches the given name value. At least 3 characters are necessary to successfully complete the query.
*
* @see SearchStringEncoder
*/
public Fetch name(String name) {
super.getUriBuilder()
.addParameter("name", name);
.addParameter("name", SearchStringEncoder.htmlEncode(name));
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.common.base.Joiner;
import de.focus_shift.lexoffice.java.sdk.LexofficeApi;
import de.focus_shift.lexoffice.java.sdk.RequestContext;
import de.focus_shift.lexoffice.java.sdk.SearchStringEncoder;
import de.focus_shift.lexoffice.java.sdk.model.Page;
import de.focus_shift.lexoffice.java.sdk.model.Voucher;
import de.focus_shift.lexoffice.java.sdk.model.VoucherStatus;
Expand Down Expand Up @@ -91,9 +92,12 @@ public VoucherListChain voucherStatus(VoucherStatus... voucherStatus) {
return this;
}

/**
* @see SearchStringEncoder
*/
public VoucherListChain voucherNumber(String voucherNumber) {
super.getUriBuilder()
.addParameter("voucherNumber", voucherNumber);
.addParameter("voucherNumber", SearchStringEncoder.htmlEncode(voucherNumber));
return this;
}

Expand All @@ -120,9 +124,12 @@ public VoucherListChain dueDate(Date dueDate) {
return this;
}

/**
* @see SearchStringEncoder
*/
public VoucherListChain contactName(String contactName) {
super.getUriBuilder()
.addParameter("contactName", contactName);
.addParameter("contactName", SearchStringEncoder.htmlEncode(contactName));
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
public enum ExecutionInterval {

WEEKLY("WEEKLY"),
BIWEEKLY("BIWEEKLY"),
MONTHLY("MONTHLY"),
QUARTERLY("QUARTERLY"),
BIANNUALLY("BIANNUALLY"),
ANNUALLY("ANNUALLY");

@Getter
Expand Down
Loading