OdooClient.java
/*
* MIT License
*
* Copyright (c) 2024 Helvethink
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.helvethink.odoo4java.xmlrpc;
import ch.helvethink.odoo4java.FetchException;
import ch.helvethink.odoo4java.models.OdooId;
import ch.helvethink.odoo4java.models.OdooObj;
import ch.helvethink.odoo4java.models.OdooObject;
import ch.helvethink.odoo4java.rpc.OdooRpcClient;
import ch.helvethink.odoo4java.serialization.OdooObjectMapper;
import ch.helvethink.odoo4java.tools.CriteriaTools;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
import org.codehaus.plexus.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.*;
import static ch.helvethink.odoo4java.serialization.OdooConstants.*;
import static ch.helvethink.odoo4java.serialization.OdooConstants.OdooMethods.*;
import static ch.helvethink.odoo4java.serialization.OdooConstants.OdooPagination.*;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
/**
* Abstraction of Odoo's XML-RPC API
*/
@SuppressWarnings({"squid:S1171", "squid:S3599", "squid:S3011"})
public class OdooClient implements OdooRpcClient {
/**
* Simple logger
*/
public static final Logger LOG = LoggerFactory.getLogger(OdooClient.class.getName());
/**
* DB name we target
*/
private final String dbName;
/**
* Password to be used when calling api (should be your api key)
*/
private final String password;
/**
* uid of logged user to the api
*/
int uid;
/**
* Custom object mapper that includes our custom deserializers
*/
private final OdooObjectMapper odooObjectMapper = new OdooObjectMapper();
/**
* Common API XML-RPC Client
*/
OdooXmlRpcClient commonClient;
/**
* Object API XML-RPC Client
*/
OdooXmlRpcClient objectXmlRpcClient;
/**
* Constructor with direct connection
*
* @param instanceUrl The odoo Instance URL
* @param dbName The odoo DB Name
* @param username The Odoo username when authenticating
* @param password The Odoo password when authenticating
* @throws MalformedURLException when URI is not valid
* @throws XmlRpcException when an error occurs with the XML-RPC API
*/
public OdooClient(final String instanceUrl, final String dbName, final String username, final String password) throws MalformedURLException, XmlRpcException {
this(instanceUrl, dbName, username, password, true);
}
/**
* Constructor that initializes the Common API XML-RPC client
*
* @param instanceUrl The odoo Instance URL
* @param dbName The odoo DB Name
* @param username The Odoo username when authenticating
* @param password The Odoo password when authenticating
* @param mustConnect - describes if we must try to connect or not
* @throws MalformedURLException when URI is not valid
*/
public OdooClient(final String instanceUrl, final String dbName, final String username, final String password, final boolean mustConnect) throws MalformedURLException {
this(new OdooXmlRpcClient() {{
setConfig(new XmlRpcClientConfigImpl() {{
setServerURL(URI.create(String.format("%s/xmlrpc/2/common", instanceUrl)).toURL());
setEnabledForExceptions(true);
setEnabledForExtensions(true);
}});
}}, instanceUrl, dbName, username, password, mustConnect);
}
/**
* Constructor with all fields
*
* @param commonClient - The common XML-RPC API client
* @param instanceUrl The odoo Instance URL
* @param dbName The odoo DB Name
* @param username The Odoo username when authenticating
* @param password The Odoo password when authenticating
* @param mustConnect - describes if we must try to connect or not
* @throws MalformedURLException when URI is not valid
*/
public OdooClient(final OdooXmlRpcClient commonClient, final String instanceUrl, final String dbName, final String username, final String password, final boolean mustConnect) throws MalformedURLException {
this.dbName = dbName;
this.password = password;
final String objectEndpoint = String.format("%s/xmlrpc/2/object", instanceUrl);
objectXmlRpcClient = new OdooXmlRpcClient() {{
setConfig(new XmlRpcClientConfigImpl() {{
setServerURL(URI.create(objectEndpoint).toURL());
}});
}};
this.commonClient = commonClient;
if (mustConnect) {
final Object authentication = commonClient.execute("authenticate", asList(dbName, username, password, emptyMap()));
// In case Authentication fail through the Odoo XML RPC Api, it sends a Boolean instead of throwing an exception
if (Boolean.FALSE.equals(authentication) || !(authentication instanceof Integer)) {
throw new FetchException("Authentication failed");
} else {
uid = (int) authentication;
LOG.info("User is {}, uid is {}", username, uid);
}
}
}
/**
* Retrieve Version from the Odoo Server
*
* @return Version like "17.0"
*/
public String getVersion() {
return ((Map<String, Object>) commonClient.execute("version", emptyList())).get("server_version").toString();
}
/**
* Retrieve all Odoo models of the Odoo instance we're connected to
*
* @param packageName - allows to filter the names of models we want to retrieve
* @return List of Odoo models with fields like "description", "name", "access_ids", ...
* @throws XmlRpcException when an error occurs with the XML-RPC API
*/
public List<Map<String, Object>> getAllModels(final String packageName) throws XmlRpcException {
final Object[] modelParams = new Object[]{
dbName, uid, password,
ODOO_INSTROSPECTION_MODEL, ODOO_SEARCH_READ_API,
emptyList(),
emptyMap()
};
final Object[] models = (Object[]) objectXmlRpcClient.execute(XML_RPC_EXECUTE_METHOD_NAME, modelParams);
return Arrays.stream(models).map(model -> ((Map<String, Object>) model))
.filter(aModel -> ((Object[]) aModel.get(MANDATORY_FIELD_FOR_ACCESSING_MODEL)).length > 0) // render only the accessible models
.filter(aModel -> aModel.get("name") == null || ((String) aModel.get("name")).startsWith(packageName))
.toList();
}
/**
* Get fields From Odoo, for a given model
*
* @param modelName The model we want to inspect
* @return The list of fields as a Map containing the following information: string, help, type, and relation
* @throws XmlRpcException when an error occurs with the XML-RPC API
*/
public Map<String, Map<String, Object>> getFields(final String modelName) throws XmlRpcException {
final Object[] params = new Object[]{dbName, uid, password, modelName, ODOO_FETCH_FIELDS_API,
emptyList(),
new HashMap<>() {{
put("attributes", ATTRIBUTES_FOR_FETCHED_FIELDS);
}}
};
return (Map<String, Map<String, Object>>) objectXmlRpcClient.execute(XML_RPC_EXECUTE_METHOD_NAME, params);
}
/**
* Find a list of object from names
*
* @param classToConvert The type of objects
* @param names The "name" value of objects we're looking for
* @param <T> The type of objects to return
* @return List of found objects
*/
public <T extends OdooObj> List<T> findByNames(final Class<T> classToConvert, final List<String> names) {
LOG.debug("Searching {} with names: {}", classToConvert.getSimpleName(), names);
return findListByIdsInt(names.stream().map(aName -> findByName(classToConvert, aName))
.filter(result -> result.length > 0) // If object is not found we don't want to get an exception
.map(result -> (Integer) ((Object[]) result[0])[0]).toList(), // id is the first field
classToConvert);
}
/**
* Fetch a single object from its name
*
* @param classToConvert The object's type
* @param aName The name of the object
* @param <T> The object's type
* @return The id of the object under its XML-RPC representation (array of objects)
*/
private <T extends OdooObj> Object[] findByName(final Class<T> classToConvert, final String aName) {
return (Object[]) objectXmlRpcClient.execute(
XML_RPC_EXECUTE_METHOD_NAME, asList(
dbName, uid, password,
classToConvert.getDeclaredAnnotation(OdooObject.class).value(), // modelName
ODOO_NAME_SEARCH_API, Collections.singletonList(aName), // Pass name as a single string
new HashMap<String, Object>() {{
put(ODOO_LIMIT, 1);
}}
)
);
}
/**
* {@inheritDoc}
*/
@Override
public <T extends OdooObj> List<T> findByCriteria(final int limit, final Class<T> classToConvert, final String... criteria) {
return findByCriteria(limit, 0, classToConvert, criteria);
}
/**
* {@inheritDoc}
*/
@Override
public <T extends OdooObj> List<T> findByCriteria(final int limit, final int page, final Class<T> classToConvert, final String... criteria) {
return findByCriteria(limit, page, "", classToConvert, criteria);
}
// TODO fix criteria!!!
@Override
public int countByCriteria(final Class<? extends OdooObj> objectType, final String... criteria) {
final List<List<List<String>>> crits = (criteria != null && criteria.length > 0) ? CriteriaTools.groupCriteria(criteria) :
List.of(List.of(asList("id", ">=", "0")));
LOG.debug("{}", crits);
return (int) objectXmlRpcClient.execute(
XML_RPC_EXECUTE_METHOD_NAME, asList(dbName, uid, password,
objectType.getDeclaredAnnotation(OdooObject.class).value(), //modelName,
"search_count", crits, new HashMap<String, Object>() {})
);
}
/**
* {@inheritDoc}
*/
@Override
public <T extends OdooObj> List<T> findByCriteria(final int limit, final int page, final String sortByField, final Class<T> classToConvert, final String... criteria) {
final List<List<List<String>>> crits = (criteria != null && criteria.length > 0) ? CriteriaTools.groupCriteria(criteria) :
List.of(List.of(asList("id", ">=", "0")));
LOG.debug("{}", crits);
final Object[] resultFromXmlRpc = (Object[]) objectXmlRpcClient.execute(
XML_RPC_EXECUTE_METHOD_NAME, asList(dbName, uid, password,
classToConvert.getDeclaredAnnotation(OdooObject.class).value(), //modelName,
ODOO_SEARCH_READ_API, crits, new HashMap<String, Object>() {{
put(ODOO_LIMIT, limit);
put(ODOO_OFFSET, page * limit);
if (!StringUtils.isEmpty(sortByField)) {
put(ODOO_SORT, sortByField);
}
}}
)
);
return Arrays.stream(resultFromXmlRpc)
.map(anObject -> odooObjectMapper.convertValue(anObject, classToConvert))
.toList();
}
/**
* {@inheritDoc}
*/
public <T extends OdooObj> T findObjectById(final OdooId idToFetch, final Class<T> classToConvert) {
if (idToFetch == null || !idToFetch.exists) {
return null;
}
final Object[] resultFromXmlRpc = (Object[]) objectXmlRpcClient.execute(
XML_RPC_EXECUTE_METHOD_NAME, asList(dbName, uid, password,
classToConvert.getDeclaredAnnotation(OdooObject.class).value(), ODOO_READ_METHOD, List.of(idToFetch.id)
));
return odooObjectMapper.convertValue(resultFromXmlRpc[0], classToConvert);
}
/**
* {@inheritDoc}
*/
@Override
public <T extends OdooObj> List<T> findListByIdsInt(final List<Integer> idsToFetch, final Class<T> classToConvert) {
if (idsToFetch == null || idsToFetch.isEmpty()) {
return Collections.emptyList();
}
try {
final Object[] resultFromXmlRpc = (Object[]) objectXmlRpcClient.execute(
XML_RPC_EXECUTE_METHOD_NAME, asList(dbName, uid, password,
classToConvert.getDeclaredAnnotation(OdooObject.class).value(), ODOO_READ_METHOD, List.of(idsToFetch)));
return Arrays.stream(resultFromXmlRpc)
.map(anObject -> odooObjectMapper.convertValue(anObject, classToConvert))
.toList();
} catch (final FetchException e) {
if (e.getMessage().contains("TypeError: dictionary key must be string")) {
LOG.error("Exception occured for class {} with ids {}", classToConvert.getSimpleName(), idsToFetch, e); // because of account.move.line
return Collections.emptyList();
} else {
throw e;
}
}
}
/**
* {@inheritDoc}
*/
public <T extends OdooObj> List<T> findListByIds(final List<OdooId> idsToFetch, final Class<T> classToConvert) {
return findListByIdsInt(idsToFetch == null ? null : idsToFetch.stream().filter(odooId -> odooId.exists).map(odooId -> odooId.id).toList(), classToConvert);
}
}