Commercial
services for your project
- Consultancy & Support
Commercial consulting requests have a guaranteed response
time. - Custom Development
We can deliver a turn key solution or contribute to your
project - Training
For fast and deep dive into framework features to shift your
team productivity.
Haulmont
we develop modern enterprise
solutions
-
Experts in Enterprise software
-
Creators of the CUBA Platform
-
Established in 2008
-
300+
developers
-
400+
projects
-
Customers in
60+
countries
Commercial
services for your project
- Consultancy & Support
Commercial consulting requests have a guaranteed response
time. - Custom Development
We can deliver a turn key solution or contribute to your
project - Training
For fast and deep dive into framework features to shift your
team productivity.
Haulmont
we develop modern enterprise
solutions
-
Experts in Enterprise software
-
Creators of the CUBA Platform
-
Established in 2008
-
300+
developers
-
400+
projects
-
Customers in
60+
countries
Платные услуги для вашего проекта
- Консалтинг и техническая поддержка
Запросы в рамках коммерческой поддержки имеют гарантированное время ответа
- Разработка на заказ
Предоставляем разработку полностью нашими рабочими ресурсами или участвуем в создании вашего проекта
- Обучение
Для быстрого и всестороннего освоения особенностей платформы, чтобы повысить продуктивность вашей команды
Haulmont
мы разрабатываем современные корпоративные решения
-
Эксперты в области разработки корпоративного ПО
-
Создатели CUBA Platform
-
Компания основана в 2008
-
300+
разработчиков
-
400+
проектов
-
Клиенты в
60+
странах
Environment
- Platform version: 6.8.13
- Client type: Web
- Long Polling: enabled
Description of the bug or enhancement
- Open App
- Open Application -> Screen
- Open Developer Console (Ctrl+Shift+F5)
After some time you can see error message in developer console:
SEVERE: Trying to start a new request while another is active
Label text stops updating each 0.5 secs and update takes 5-7 seconds for update this text.
When update occurs it refresh whole UI and you can see blink on label text.
On screen there are several background tasks which restarts by timer event. You can see in developer console that push requests are sending to server. After some time (random time) UI receives answer on push request with wrong server id and decide to resync whole state. After that push requests stop working and UI updates only with using resync which updates whole UI. It leads to blinking of UI components.
In developer console you can see:
INFO: Received message with server id 1103 but expected 1099. Postponing handling until the missing message(s) have been received
WARNING: Gave up waiting for message 1099 from the server
INFO: Resynchronizing from server
SEVERE: Trying to start a new request while another is active
I’ve attached sample project with this issue.
push-example.zip
Java developers have access to a number of useful tools that help to write high-quality code such as the powerful IDE IntelliJ IDEA, free analyzers SpotBugs, PMD, and the like. The developers working on CUBA Platform have already been using all of these, and this review will show how the project can benefit even more from the use of the static code analyzer PVS-Studio.
A few words about the project and the analyzer
CUBA Platform is a high-level framework for enterprise application development. The platform abstracts developers from underlying technologies so they can focus on the business tasks, while retaining full flexibility by providing unrestricted access to low-level code. The source code was downloaded from GitHub.
PVS-Studio is a tool for detecting bugs and potential security vulnerabilities in the source code of programs written in C, C++, C#, and Java. The analyzer runs on 64-bit Windows, Linux, and macOS systems. To make things easier for Java programmers, we developed plugins for Maven, Gradle, and IntelliJ IDEA. I checked the project using the Gradle plugin, and it went off without a hitch.
Errors in conditions
Warning 1
V6007 Expression ‘StringUtils.isNotEmpty(«handleTabKey»)’ is always true. SourceCodeEditorLoader.java(60)
@Override
public void loadComponent() {
....
String handleTabKey = element.attributeValue("handleTabKey");
if (StringUtils.isNotEmpty("handleTabKey")) {
resultComponent.setHandleTabKey(Boolean.parseBoolean(handleTabKey));
}
....
}
The attribute value extracted from the element is not checked. Instead, the isNotEmpty function gets a string literal as its argument rather than the variable handleTabKey.
A similar error found in the file AbstractTableLoader.java:
- V6007 Expression ‘StringUtils.isNotEmpty(«editable»)’ is always true. AbstractTableLoader.java(596)
Warning 2
V6007 Expression ‘previousMenuItemFlatIndex >= 0’ is always true. CubaSideMenuWidget.java(328)
protected MenuItemWidget findNextMenuItem(MenuItemWidget currentItem) {
List<MenuTreeNode> menuTree = buildVisibleTree(this);
List<MenuItemWidget> menuItemWidgets = menuTreeToList(menuTree);
int menuItemFlatIndex = menuItemWidgets.indexOf(currentItem);
int previousMenuItemFlatIndex = menuItemFlatIndex + 1;
if (previousMenuItemFlatIndex >= 0) {
return menuItemWidgets.get(previousMenuItemFlatIndex);
}
return null;
}
The indexOf function will return -1 if the element is not found in the list. The value 1 is then added to the index, which disguises the problem with the absent element. Another potential problem has to do with the fact that the previousMenuItemFlatIndex variable will always be greater than or equal to zero. For example, if the menuItemWidgets list is found to be empty, the program will end up with an array overrun.
Warning 3
V6009 The ‘delete’ function could receive the ‘-1’ value while non-negative value is expected. Inspect argument: 1. AbstractCollectionDatasource.java(556)
protected DataLoadContextQuery createDataQuery(....) {
....
StringBuilder orderBy = new StringBuilder();
....
if (orderBy.length() > 0) {
orderBy.delete(orderBy.length() - 2, orderBy.length());
orderBy.insert(0, " order by ");
}
....
}
The last two characters of the orderBy buffer are deleted if the total number of elements is greater than zero, i.e. if the string contains at least one character. However, the start position from where the deletion begins is offset by 2. So, if orderBy happens to contain one character, attempting to delete it will raise a StringIndexOutOfBoundsException.
Warning 4
V6013 Objects ‘masterCollection’ and ‘entities’ are compared by reference. Possibly an equality comparison was intended. CollectionPropertyContainerImpl.java(81)
@Override
public void setItems(@Nullable Collection<E> entities) {
super.setItems(entities);
Entity masterItem = master.getItemOrNull();
if (masterItem != null) {
MetaProperty masterProperty = getMasterProperty();
Collection masterCollection = masterItem.getValue(masterProperty.getName());
if (masterCollection != entities) {
updateMasterCollection(masterProperty, masterCollection, entities);
}
}
}
In the updateMasterCollection function, the values from entities are copied to masterCollection. One line earlier, the collections have been compared by reference, but the programmer probably intended it to be a comparison by value.
Warning 5
V6013 Objects ‘value’ and ‘oldValue’ are compared by reference. Possibly an equality comparison was intended. WebOptionsList.java(278)
protected boolean isCollectionValuesChanged(Collection<I> value,
Collection<I> oldValue) {
return value != oldValue;
}
This case is similar to the previous one. The collections are compared in the isCollectionValuesChanged function, and reference comparison is perhaps not what was intended here either.
Redundant conditions
Warning 1
V6007 Expression ‘mask.charAt(i + offset) != placeHolder’ is always true. DatePickerDocument.java(238)
private String calculateFormattedString(int offset, String text) .... {
....
if ((mask.charAt(i + offset) == placeHolder)) { // <=
....
} else if ((mask.charAt(i + offset) != placeHolder) && // <=
(Character.isDigit(text.charAt(i)))) {
....
}
....
}
The second condition checks an expression that is opposite to the one checked in the first condition. The latter can, therefore, be safely removed to shorten the code.
V6007 Expression ‘connector == null’ is always false. HTML5Support.java(169)
private boolean validate(NativeEvent event) {
....
while (connector == null) {
widget = widget.getParent();
connector = Util.findConnectorFor(widget);
}
if (this.connector == connector) {
return true;
} else if (connector == null) { // <=
return false;
} else if (connector.getWidget() instanceof VDDHasDropHandler) {
return false;
}
return true;
}
After leaving the while loop, the value of the connector variable won’t be equal to null, so the redundant check can be deleted.
Another suspicious warning of this type that needs to be examined:
- V6007 Expression ‘StringUtils.isBlank(strValue)’ is always true. Param.java(818)
Unreachable code in tests
V6019 Unreachable code detected. It is possible that an error is present. TransactionTest.java(283)
private void throwException() {
throw new RuntimeException(TEST_EXCEPTION_MSG);
}
@Test
public void testSuspendRollback() {
Transaction tx = cont.persistence().createTransaction();
try {
....
Transaction tx1 = cont.persistence().createTransaction();
try {
EntityManager em1 = cont.persistence().getEntityManager();
assertTrue(em != em1);
Server server1 = em1.find(Server.class, server.getId());
assertNull(server1);
throwException(); // <=
tx1.commit(); // <=
} catch (Exception e) {
//
} finally {
tx1.end();
}
tx.commit();
} finally {
tx.end();
}
}
The throwException function throws an exception that prevents execution of the call of tx1.commit. Those two lines should be swapped for the code to work properly.
There were a few similar problems in other tests too:
- V6019 Unreachable code detected. It is possible that an error is present. TransactionTest.java(218)
- V6019 Unreachable code detected. It is possible that an error is present. TransactionTest.java(163)
- V6019 Unreachable code detected. It is possible that an error is present. TransactionTest.java(203)
- V6019 Unreachable code detected. It is possible that an error is present. TransactionTest.java(137)
- V6019 Unreachable code detected. It is possible that an error is present. UpdateDetachedTest.java(153)
- V6019 Unreachable code detected. It is possible that an error is present. EclipseLinkDetachedTest.java(132)
- V6019 Unreachable code detected. It is possible that an error is present. PersistenceTest.java(223)
Suspicious arguments
Warning 1
V6023 Parameter ‘salt’ is always rewritten in method body before being used. BCryptEncryptionModule.java(47)
@Override
public String getHash(String content, String salt) {
salt = BCrypt.gensalt();
return BCrypt.hashpw(content, salt);
}
In cryptography, salt is a data string that you pass along with the password to a hash function. It is mainly used to protect the program against dictionary attacks and rainbow table attacks, as well as to obscure identical passwords. More here: Salt (cryptography).
In this function, the passed string is overwritten right after entering. Ignoring the value passed to the function is a potential vulnerability.
Warning 2
This function triggered two warnings at once:
- V6023 Parameter ‘offsetWidth’ is always rewritten in method body before being used. CubaSuggestionFieldWidget.java(433)
- V6023 Parameter ‘offsetHeight’ is always rewritten in method body before being used. CubaSuggestionFieldWidget.java(433)
@Override
public void setPosition(int offsetWidth, int offsetHeight) {
offsetHeight = getOffsetHeight();
....
if (offsetHeight + getPopupTop() > ....)) {
....
}
....
offsetWidth = containerFirstChild.getOffsetWidth();
if (offsetWidth + getPopupLeft() > ....)) {
....
} else {
left = getPopupLeft();
}
setPopupPosition(left, top);
}
That’s quite a curious snippet. The function is called with only two variables as arguments, offsetWidth and offsetHeight, and both are overwritten before use.
Warning 3
V6022 Parameter ‘shortcut’ is not used inside constructor body. DeclarativeTrackingAction.java(47)
public DeclarativeTrackingAction(String id, String caption, String description,
String icon, String enable, String visible,
String methodName, @Nullable String shortcut,
ActionsHolder holder) {
super(id);
this.caption = caption;
this.description = description;
this.icon = icon;
setEnabled(enable == null || Boolean.parseBoolean(enable));
setVisible(visible == null || Boolean.parseBoolean(visible));
this.methodName = methodName;
checkActionsHolder(holder);
}
The function doesn’t make use of the value passed as the shortcut parameter. Maybe the function’s interface has become obsolete, or this warning is just a false positive.
A few more defects of this type:
- V6022 Parameter ‘type’ is not used inside constructor body. QueryNode.java(36)
- V6022 Parameter ‘text2’ is not used inside constructor body. MarkerAddition.java(22)
- V6022 Parameter ‘selection’ is not used inside constructor body. AceEditor.java(114)
- V6022 Parameter ‘options’ is not used inside constructor body. EntitySerialization.java(379)
Different functions, same code
Warning 1
V6032 It is odd that the body of method ‘firstItemId’ is fully equivalent to the body of another method ‘lastItemId’. ContainerTableItems.java(213), ContainerTableItems.java(219)
@Override
public Object firstItemId() {
List<E> items = container.getItems();
return items.isEmpty() ? null : items.get(0).getId();
}
@Override
public Object lastItemId() {
List<E> items = container.getItems();
return items.isEmpty() ? null : items.get(0).getId();
}
The functions firstItemId and lastItemId have the same implementations. The latter was probably meant to get the index of the last element rather than get the element at index 0.
Warning 2
V6032 It is odd that the body of method is fully equivalent to the body of another method. SearchComboBoxPainter.java(495), SearchComboBoxPainter.java(501)
private void paintBackgroundDisabledAndEditable(Graphics2D g) {
rect = decodeRect1();
g.setPaint(color53);
g.fill(rect);
}
private void paintBackgroundEnabledAndEditable(Graphics2D g) {
rect = decodeRect1();
g.setPaint(color53);
g.fill(rect);
}
Two more functions with suspiciously identical bodies. My guess is that one of them was meant to work with some other color instead of color53.
Null dereference
Warning 1
V6060 The ‘descriptionPopup’ reference was utilized before it was verified against null. SuggestPopup.java(252), SuggestPopup.java(251)
protected void updateDescriptionPopupPosition() {
int x = getAbsoluteLeft() + WIDTH;
int y = getAbsoluteTop();
descriptionPopup.setPopupPosition(x, y);
if (descriptionPopup!=null) {
descriptionPopup.setPopupPosition(x, y);
}
}
In just two lines, the programmer managed to write a highly suspicious piece of code. First the method setPopupPosition of the object descriptionPopup is called, and then the object is checked for null. The first call to setPopupPosition is probably redundant and potentially dangerous. I guess it results from bad refactoring.
Warning 2
V6060 The ‘tableModel’ reference was utilized before it was verified against null. DesktopAbstractTable.java(1580), DesktopAbstractTable.java(1564)
protected Column addRuntimeGeneratedColumn(String columnId) {
// store old cell editors / renderers
TableCellEditor[] cellEditors =
new TableCellEditor[tableModel.getColumnCount() + 1]; // <=
TableCellRenderer[] cellRenderers =
new TableCellRenderer[tableModel.getColumnCount() + 1]; // <=
for (int i = 0; i < tableModel.getColumnCount(); i++) { // <=
Column tableModelColumn = tableModel.getColumn(i);
if (tableModel.isGeneratedColumn(tableModelColumn)) { // <=
TableColumn tableColumn = getColumn(tableModelColumn);
cellEditors[i] = tableColumn.getCellEditor();
cellRenderers[i] = tableColumn.getCellRenderer();
}
}
Column col = new Column(columnId, columnId);
col.setEditable(false);
columns.put(col.getId(), col);
if (tableModel != null) { // <=
tableModel.addColumn(col);
}
....
}
This case is similar to the previous one. By the time the tableModel object is checked for null, it has already been accessed multiple times.
Another example:
- V6060 The ‘tableModel’ reference was utilized before it was verified against null. DesktopAbstractTable.java(596), DesktopAbstractTable.java(579)
Probably a logic error
V6026 This value is already assigned to the ‘sortAscending’ variable. CubaScrollTableWidget.java(488)
@Override
protected void sortColumn() {
....
if (sortAscending) {
if (sortClickCounter < 2) {
// special case for initial revert sorting instead of reset sort order
if (sortClickCounter == 0) {
client.updateVariable(paintableId, "sortascending", false, false);
} else {
reloadDataFromServer = false;
sortClickCounter = 0;
sortColumn = null;
sortAscending = true; // <=
client.updateVariable(paintableId, "resetsortorder", "", true);
}
} else {
client.updateVariable(paintableId, "sortascending", false, false);
}
} else {
if (sortClickCounter < 2) {
// special case for initial revert sorting instead of reset sort order
if (sortClickCounter == 0) {
client.updateVariable(paintableId, "sortascending", true, false);
} else {
reloadDataFromServer = false;
sortClickCounter = 0;
sortColumn = null;
sortAscending = true;
client.updateVariable(paintableId, "resetsortorder", "", true);
}
} else {
reloadDataFromServer = false;
sortClickCounter = 0;
sortColumn = null;
sortAscending = true;
client.updateVariable(paintableId, "resetsortorder", "", true);
}
}
....
}
In the first condition, the variable sortAscending has already been assigned the value true, but it’s still assigned the same value again later on. This must be a mistake, and the author probably meant the value false.
A similar example from a different file:
- V6026 This value is already assigned to the ‘sortAscending’ variable. CubaTreeTableWidget.java(444)
Strange return values
Warning 1
V6037 An unconditional ‘return’ within a loop. QueryCacheManager.java(128)
public <T> T getSingleResultFromCache(QueryKey queryKey, List<View> views) {
....
for (Object id : queryResult.getResult()) {
return (T) em.find(metaClass.getJavaClass(), id, views.toArray(....));
}
....
}
The analyzer has detected an unconditional call to return at the very first iteration of a for loop. Either that line is incorrect or the loop should be rewritten as an if statement.
Warning 2
V6014 It’s odd that this method always returns one and the same value. DefaultExceptionHandler.java(40)
@Override
public boolean handle(ErrorEvent event, App app) {
Throwable t = event.getThrowable();
if (t instanceof SocketException
|| ExceptionUtils.getRootCause(t) instanceof SocketException) {
return true;
}
if (ExceptionUtils.getThrowableList(t).stream()
.anyMatch(o -> o.getClass().getName().equals("...."))) {
return true;
}
if (StringUtils.contains(ExceptionUtils.getMessage(t), "....")) {
return true;
}
AppUI ui = AppUI.getCurrent();
if (ui == null) {
return true;
}
if (t != null) {
if (app.getConnection().getSession() != null) {
showDialog(app, t);
} else {
showNotification(app, t);
}
}
return true;
}
This function returns true in each case, while the last line obviously calls for false. It looks like a mistake.
Here’s a full list of other similar suspicious functions:
- V6014 It’s odd that this method always returns one and the same value. ErrorNodesFinder.java(31)
- V6014 It’s odd that this method always returns one and the same value. FileDownloadController.java(69)
- V6014 It’s odd that this method always returns one and the same value. IdVarSelector.java(73)
- V6014 It’s odd that this method always returns one and the same value. IdVarSelector.java(48)
- V6014 It’s odd that this method always returns one and the same value. IdVarSelector.java(67)
- V6014 It’s odd that this method always returns one and the same value. IdVarSelector.java(46)
- V6014 It’s odd that this method always returns one and the same value. JoinVariableNode.java(57)
Warning 3
V6007 Expression ‘needReload’ is always false. WebAbstractTable.java(2702)
protected boolean handleSpecificVariables(Map<String, Object> variables) {
boolean needReload = false;
if (isUsePresentations() && presentations != null) {
Presentations p = getPresentations();
if (p.getCurrent() != null && p.isAutoSave(p.getCurrent())
&& needUpdatePresentation(variables)) {
Element e = p.getSettings(p.getCurrent());
saveSettings(e);
p.setSettings(p.getCurrent(), e);
}
}
return needReload;
}
The function returns the needReload variable whose value is always false. Some code for changing that value is probably missing from one of the conditions.
Warning 4
V6062 Possible infinite recursion inside the ‘isFocused’ method. GwtAceEditor.java(189), GwtAceEditor.java(190)
public final native void focus() /*-{
this.focus();
}-*/;
public final boolean isFocused() {
return this.isFocused();
}
The analyzer has detected a recursive function with no stop condition. This file contains a lot of functions marked with the keyword native and containing commented-out code. The developers are probably rewriting this file now and will soon notice the isFocused function too.
Miscellaneous
Warning 1
V6002 The switch statement does not cover all values of the ‘Operation’ enum: ADD. DesktopAbstractTable.java(665)
/**
* Operation which caused the datasource change.
*/
enum Operation {
REFRESH,
CLEAR,
ADD,
REMOVE,
UPDATE
}
@Override
public void setDatasource(final CollectionDatasource datasource) {
....
collectionChangeListener = e -> {
switch (e.getOperation()) {
case CLEAR:
case REFRESH:
fieldDatasources.clear();
break;
case UPDATE:
case REMOVE:
for (Object entity : e.getItems()) {
fieldDatasources.remove(entity);
}
break;
}
};
....
}
The switch statement has no case for the value ADD. It’s the only value that’s not being checked, so the developers should take a look at this code.
Warning 2
V6021 Variable ‘source’ is not used. DefaultHorizontalLayoutDropHandler.java(177)
@Override
protected void handleHTML5Drop(DragAndDropEvent event) {
LayoutBoundTransferable transferable = (LayoutBoundTransferable) event
.getTransferable();
HorizontalLayoutTargetDetails details = (HorizontalLayoutTargetDetails) event
.getTargetDetails();
AbstractOrderedLayout layout = (AbstractOrderedLayout) details
.getTarget();
Component source = event.getTransferable().getSourceComponent(); // <=
int idx = (details).getOverIndex();
HorizontalDropLocation loc = (details).getDropLocation();
if (loc == HorizontalDropLocation.CENTER
|| loc == HorizontalDropLocation.RIGHT) {
idx++;
}
Component comp = resolveComponentFromHTML5Drop(event);
if (idx >= 0) {
layout.addComponent(comp, idx);
} else {
layout.addComponent(comp);
}
if (dropAlignment != null) {
layout.setComponentAlignment(comp, dropAlignment);
}
}
The variable source is declared but not used. Perhaps the authors forgot to add source to layout, just like it happened with another variable of this type, comp.
Other functions with unused variables:
- V6021 Variable ‘source’ is not used. DefaultHorizontalLayoutDropHandler.java(175)
- V6021 The value is assigned to the ‘r’ variable but is not used. ExcelExporter.java(262)
- V6021 Variable ‘over’ is not used. DefaultCssLayoutDropHandler.java(49)
- V6021 Variable ‘transferable’ is not used. DefaultHorizontalLayoutDropHandler.java(171)
- V6021 Variable ‘transferable’ is not used. DefaultHorizontalLayoutDropHandler.java(169)
- V6021 Variable ‘beanLocator’ is not used. ScreenEventMixin.java(28)
Warning 3
V6054 Classes should not be compared by their name. MessageTools.java(283)
public boolean hasPropertyCaption(MetaProperty property) {
Class<?> declaringClass = property.getDeclaringClass();
if (declaringClass == null)
return false;
String caption = getPropertyCaption(property);
int i = caption.indexOf('.');
if (i > 0 && declaringClass.getSimpleName().equals(caption.substring(0, i)))
return false;
else
return true;
}
The analyzer has detected a class comparison by name. It’s incorrect to compare classes by name as, according to the specification, the names of JVM classes must be unique only within a package. Such a comparison yields incorrect results and leads to executing the wrong code.
Comment by CUBA Platform developers
Any large project surely has bugs in it. Knowing that, we gladly agreed when the PVS-Studio team offered to check our project. The CUBA repository contains forks of some of the third-party OSS libraries licensed under Apache 2, and it looks like we should pay more attention to that code as the analyzer found quite a number of problems in those sources. We currently use SpotBugs as our primary analyzer, and it fails to notice some of the big bugs reported by PVS-Studio. It seems we should write some additional diagnostics ourselves. Many thanks to the PVS-Studio team for the job.
The developers also told us that the warnings V6013 and V6054 were false positives; it was their conscious decision to write that code the way they did. The analyzer is designed for detecting suspicious code fragments, and the probability of finding genuine bugs varies across different diagnostics. Such warnings, however, can be easily handled using the special mass warning suppression mechanism without having to modify the source files.
Also, the PVS-Studio team cannot take no notice of the phrase «it seems we should write some additional diagnostics ourselves» and do without this picture
Conclusion
PVS-Studio can be a perfect complement to existing quality-control tools used in your development process. It’s especially true for companies with dozens, hundreds, or thousands of developers. PVS-Studio is designed not only to detect bugs but also to help you fix them, and what I mean by that is not automatic code editing but reliable means of code quality control. In a large company, it’s impossible for every developer to check their respective parts of the code with different tools, so a better solution for such companies would be to adopt tools like PVS-Studio, which provide code quality control at every development stage, not only on the programmer side.
- О проекте и анализаторе
- Ошибки в условиях
- Избыточные условия
- Недостижимый код в тестах
- Подозрительные аргументы функций
- Разные функции с одинаковым кодом
- Обращение по нулевой ссылке
- Возможно, логическая ошибка
- Странные возвращаемые значения функций
- Разные предупреждения
- Отзыв разработчиков CUBA Platform
- Заключение
Для Java программистов существуют полезные инструменты, помогающие писать качественный код, например, мощная среда разработки IntelliJ IDEA, бесплатные анализаторы SpotBugs, PMD и другие. Всё это уже используется в разработке проекта CUBA Platform, и в этом обзоре найденных дефектов кода я расскажу, как ещё можно улучшить качество проекта, используя статический анализатор кода PVS-Studio.
О проекте и анализаторе
CUBA Platform — это высокоуровневый Java-фреймворк для быстрого создания корпоративных приложений с полноценным веб-интерфейсом. Платформа абстрагирует разработчика от разнородных технологий, чтобы вы могли сфокусироваться на решении задач бизнеса, в то же время, не лишая возможности работать с ними напрямую. Исходный код взят из репозитория на GitHub.
PVS-Studio — это инструмент для выявления ошибок и потенциальных уязвимостей в исходном коде программ, написанных на языках C, C++, C# и Java. Работает в 64-битных системах на Windows, Linux и macOS. Для удобства Java-программистов мы разработали плагины для Maven, Gradle и IntelliJ IDEA. Проект CUBA Platform легко проверился с помощью плагина для Gradle.
Ошибки в условиях
Предупреждение 1
V6007 Expression ‘StringUtils.isNotEmpty(«handleTabKey»)’ is always true. SourceCodeEditorLoader.java(60)
@Override
public void loadComponent() {
....
String handleTabKey = element.attributeValue("handleTabKey");
if (StringUtils.isNotEmpty("handleTabKey")) {
resultComponent.setHandleTabKey(Boolean.parseBoolean(handleTabKey));
}
....
}
После извлечения значения атрибута из некого элемента не выполняется проверка этого значения. Вместо этого в функцию isNotEmpty передаётся константная строка, а надо было передать переменную handleTabKey.
Есть ещё одна аналогичная ошибка в файле AbstractTableLoader.java:
- V6007 Expression ‘StringUtils.isNotEmpty(«editable»)’ is always true. AbstractTableLoader.java(596)
Предупреждение 2
V6007 Expression ‘previousMenuItemFlatIndex >= 0’ is always true. CubaSideMenuWidget.java(328)
protected MenuItemWidget findNextMenuItem(MenuItemWidget currentItem) {
List<MenuTreeNode> menuTree = buildVisibleTree(this);
List<MenuItemWidget> menuItemWidgets = menuTreeToList(menuTree);
int menuItemFlatIndex = menuItemWidgets.indexOf(currentItem);
int previousMenuItemFlatIndex = menuItemFlatIndex + 1;
if (previousMenuItemFlatIndex >= 0) {
return menuItemWidgets.get(previousMenuItemFlatIndex);
}
return null;
}
Функция indexOf может вернуть значение -1, если в списке не будет найден элемент. Затем к индексу прибавляется единица, скрывая таким образом ситуацию, когда нужный элемент отсутствует. Другой потенциальной проблемой может стать тот факт, что переменная previousMenuItemFlatIndex будет всегда больше или равна нулю. Если, например, список menuItemWidgets будет пустым, то становится возможным выход за границу массива.
Предупреждение 3
V6009 The ‘delete’ function could receive the ‘-1’ value while non-negative value is expected. Inspect argument: 1. AbstractCollectionDatasource.java(556)
protected DataLoadContextQuery createDataQuery(....) {
....
StringBuilder orderBy = new StringBuilder();
....
if (orderBy.length() > 0) {
orderBy.delete(orderBy.length() - 2, orderBy.length());
orderBy.insert(0, " order by ");
}
....
}
В буфере символов orderBy удаляют последние 2 символа, если их общее количество больше нуля, т.е. строка содержит один символ или больше. Но стартовую позицию удаления символов задали со смещением на 2 символа. Таким образом, если вдруг orderBy будет состоять из 1 символа, попытка удаления приведёт к исключению StringIndexOutOfBoundsException.
Предупреждение 4
V6013 Objects ‘masterCollection’ and ‘entities’ are compared by reference. Possibly an equality comparison was intended. CollectionPropertyContainerImpl.java(81)
@Override
public void setItems(@Nullable Collection<E> entities) {
super.setItems(entities);
Entity masterItem = master.getItemOrNull();
if (masterItem != null) {
MetaProperty masterProperty = getMasterProperty();
Collection masterCollection = masterItem.getValue(masterProperty.getName());
if (masterCollection != entities) {
updateMasterCollection(masterProperty, masterCollection, entities);
}
}
}
В функции updateMasterCollection значения из entities копируются в masterCollection. Перед этим коллекции сравнили по ссылке, но, возможно, их планировали сравнивать по значению.
Предупреждение 5
V6013 Objects ‘value’ and ‘oldValue’ are compared by reference. Possibly an equality comparison was intended. WebOptionsList.java(278)
protected boolean isCollectionValuesChanged(Collection<I> value,
Collection<I> oldValue) {
return value != oldValue;
}
Этот пример похож на предыдущий. Здесь isCollectionValuesChanged предназначена для сравнения коллекций. Возможно, сравнение по ссылке тоже не тот способ, который ожидался.
Избыточные условия
Предупреждение 1
V6007 Expression ‘mask.charAt(i + offset) != placeHolder’ is always true. DatePickerDocument.java(238)
private String calculateFormattedString(int offset, String text) .... {
....
if ((mask.charAt(i + offset) == placeHolder)) { // <=
....
} else if ((mask.charAt(i + offset) != placeHolder) && // <=
(Character.isDigit(text.charAt(i)))) {
....
}
....
}
Во втором сравнении проверяется выражение, которое противоположно первому. Вторую проверку можно удалить, что упростит код.
V6007 Expression ‘connector == null’ is always false. HTML5Support.java(169)
private boolean validate(NativeEvent event) {
....
while (connector == null) {
widget = widget.getParent();
connector = Util.findConnectorFor(widget);
}
if (this.connector == connector) {
return true;
} else if (connector == null) { // <=
return false;
} else if (connector.getWidget() instanceof VDDHasDropHandler) {
return false;
}
return true;
}
После завершения цикла while, значение переменной connector не будет равняться null, следовательно, избыточную проверку можно удалить.
Ещё одно подозрительное место, на которое стоит обратить внимание разработчикам:
- V6007 Expression ‘StringUtils.isBlank(strValue)’ is always true. Param.java(818)
Недостижимый код в тестах
V6019 Unreachable code detected. It is possible that an error is present. TransactionTest.java(283)
private void throwException() {
throw new RuntimeException(TEST_EXCEPTION_MSG);
}
@Test
public void testSuspendRollback() {
Transaction tx = cont.persistence().createTransaction();
try {
....
Transaction tx1 = cont.persistence().createTransaction();
try {
EntityManager em1 = cont.persistence().getEntityManager();
assertTrue(em != em1);
Server server1 = em1.find(Server.class, server.getId());
assertNull(server1);
throwException(); // <=
tx1.commit(); // <=
} catch (Exception e) {
//
} finally {
tx1.end();
}
tx.commit();
} finally {
tx.end();
}
}
Функция throwException бросает исключение, из-за которого не выполняется вызов функции tx1.commit. Возможно, эти строки стоит поменять местами.
Ещё несколько похожих мест в других тестах:
- V6019 Unreachable code detected. It is possible that an error is present. TransactionTest.java(218)
- V6019 Unreachable code detected. It is possible that an error is present. TransactionTest.java(163)
- V6019 Unreachable code detected. It is possible that an error is present. TransactionTest.java(203)
- V6019 Unreachable code detected. It is possible that an error is present. TransactionTest.java(137)
- V6019 Unreachable code detected. It is possible that an error is present. UpdateDetachedTest.java(153)
- V6019 Unreachable code detected. It is possible that an error is present. EclipseLinkDetachedTest.java(132)
- V6019 Unreachable code detected. It is possible that an error is present. PersistenceTest.java(223)
Подозрительные аргументы функций
Предупреждение 1
V6023 Parameter ‘salt’ is always rewritten in method body before being used. BCryptEncryptionModule.java(47)
@Override
public String getHash(String content, String salt) {
salt = BCrypt.gensalt();
return BCrypt.hashpw(content, salt);
}
В криптографии salt — строка данных, которая передаётся хеш-функции вместе с паролем. Главным образом используется для защиты от перебора по словарю и атак с использованием радужных таблиц, а также сокрытия одинаковых паролей. Подробнее: Соль (криптография).
В этой функции строка перетирается сразу при входе в функцию. Возможно, игнорирование переданного значения является потенциальной уязвимостью.
Предупреждение 2
Для рассматриваемой функции анализатор выдаёт сразу два предупреждения:
- V6023 Parameter ‘offsetWidth’ is always rewritten in method body before being used. CubaSuggestionFieldWidget.java(433)
- V6023 Parameter ‘offsetHeight’ is always rewritten in method body before being used. CubaSuggestionFieldWidget.java(433)
@Override
public void setPosition(int offsetWidth, int offsetHeight) {
offsetHeight = getOffsetHeight();
....
if (offsetHeight + getPopupTop() > ....)) {
....
}
....
offsetWidth = containerFirstChild.getOffsetWidth();
if (offsetWidth + getPopupLeft() > ....)) {
....
} else {
left = getPopupLeft();
}
setPopupPosition(left, top);
}
Занятный код. Функция принимает всего две переменных: offsetWidth и offsetHeight, обе перезаписываются перед использованием.
Предупреждение 3
V6022 Parameter ‘shortcut’ is not used inside constructor body. DeclarativeTrackingAction.java(47)
public DeclarativeTrackingAction(String id, String caption, String description,
String icon, String enable, String visible,
String methodName, @Nullable String shortcut,
ActionsHolder holder) {
super(id);
this.caption = caption;
this.description = description;
this.icon = icon;
setEnabled(enable == null || Boolean.parseBoolean(enable));
setVisible(visible == null || Boolean.parseBoolean(visible));
this.methodName = methodName;
checkActionsHolder(holder);
}
Значение параметра shortcut не используется в функции. Возможно, интерфейс функции устарел или это предупреждение не является ошибкой.
Ещё несколько подобных мест:
- V6022 Parameter ‘type’ is not used inside constructor body. QueryNode.java(36)
- V6022 Parameter ‘text2’ is not used inside constructor body. MarkerAddition.java(22)
- V6022 Parameter ‘selection’ is not used inside constructor body. AceEditor.java(114)
- V6022 Parameter ‘options’ is not used inside constructor body. EntitySerialization.java(379)
Разные функции с одинаковым кодом
Предупреждение 1
V6032 It is odd that the body of method ‘firstItemId’ is fully equivalent to the body of another method ‘lastItemId’. ContainerTableItems.java(213), ContainerTableItems.java(219)
@Override
public Object firstItemId() {
List<E> items = container.getItems();
return items.isEmpty() ? null : items.get(0).getId();
}
@Override
public Object lastItemId() {
List<E> items = container.getItems();
return items.isEmpty() ? null : items.get(0).getId();
}
Функции firstItemId и lastItemId имеют одинаковую реализацию. Скорее всего, в последней необходимо было получать элемент не с индексом 0, а вычислять индекс последнего элемента.
Предупреждение 2
V6032 It is odd that the body of method is fully equivalent to the body of another method. SearchComboBoxPainter.java(495), SearchComboBoxPainter.java(501)
private void paintBackgroundDisabledAndEditable(Graphics2D g) {
rect = decodeRect1();
g.setPaint(color53);
g.fill(rect);
}
private void paintBackgroundEnabledAndEditable(Graphics2D g) {
rect = decodeRect1();
g.setPaint(color53);
g.fill(rect);
}
Ещё две функции с подозрительно одинаковой реализацией. Рискну предположить, что в одной из них нужно было использовать другой цвет, отличный от color53.
Обращение по нулевой ссылке
Предупреждение 1
V6060 The ‘descriptionPopup’ reference was utilized before it was verified against null. SuggestPopup.java(252), SuggestPopup.java(251)
protected void updateDescriptionPopupPosition() {
int x = getAbsoluteLeft() + WIDTH;
int y = getAbsoluteTop();
descriptionPopup.setPopupPosition(x, y);
if (descriptionPopup!=null) {
descriptionPopup.setPopupPosition(x, y);
}
}
Всего в двух строчках автору удалось написать очень подозрительный код. Сначала у объекта descriptionPopup вызывается метод setPopupPosition, а потом объект сравнивается с null. Скорее всего, первый вызов функции setPopupPosition является лишним и опасным. Похоже на последствия неудачного рефакторинга.
Предупреждения 2
V6060 The ‘tableModel’ reference was utilized before it was verified against null. DesktopAbstractTable.java(1580), DesktopAbstractTable.java(1564)
protected Column addRuntimeGeneratedColumn(String columnId) {
// store old cell editors / renderers
TableCellEditor[] cellEditors =
new TableCellEditor[tableModel.getColumnCount() + 1]; // <=
TableCellRenderer[] cellRenderers =
new TableCellRenderer[tableModel.getColumnCount() + 1]; // <=
for (int i = 0; i < tableModel.getColumnCount(); i++) { // <=
Column tableModelColumn = tableModel.getColumn(i);
if (tableModel.isGeneratedColumn(tableModelColumn)) { // <=
TableColumn tableColumn = getColumn(tableModelColumn);
cellEditors[i] = tableColumn.getCellEditor();
cellRenderers[i] = tableColumn.getCellRenderer();
}
}
Column col = new Column(columnId, columnId);
col.setEditable(false);
columns.put(col.getId(), col);
if (tableModel != null) { // <=
tableModel.addColumn(col);
}
....
}
Похожая ситуация и в этой функции. После многочисленных обращений к объекту tableModel выполняется проверка, равен он null или нет.
Ещё один пример:
- V6060 The ‘tableModel’ reference was utilized before it was verified against null. DesktopAbstractTable.java(596), DesktopAbstractTable.java(579)
Возможно, логическая ошибка
V6026 This value is already assigned to the ‘sortAscending’ variable. CubaScrollTableWidget.java(488)
@Override
protected void sortColumn() {
....
if (sortAscending) {
if (sortClickCounter < 2) {
// special case for initial revert sorting instead of reset sort order
if (sortClickCounter == 0) {
client.updateVariable(paintableId, "sortascending", false, false);
} else {
reloadDataFromServer = false;
sortClickCounter = 0;
sortColumn = null;
sortAscending = true; // <=
client.updateVariable(paintableId, "resetsortorder", "", true);
}
} else {
client.updateVariable(paintableId, "sortascending", false, false);
}
} else {
if (sortClickCounter < 2) {
// special case for initial revert sorting instead of reset sort order
if (sortClickCounter == 0) {
client.updateVariable(paintableId, "sortascending", true, false);
} else {
reloadDataFromServer = false;
sortClickCounter = 0;
sortColumn = null;
sortAscending = true;
client.updateVariable(paintableId, "resetsortorder", "", true);
}
} else {
reloadDataFromServer = false;
sortClickCounter = 0;
sortColumn = null;
sortAscending = true;
client.updateVariable(paintableId, "resetsortorder", "", true);
}
}
....
}
В первом условии переменная sortAscending и так равна true, но ей всё равно присваивают то же самое значение. Возможно, это является ошибкой и хотели присвоить false.
Похожий пример из другого файла:
- V6026 This value is already assigned to the ‘sortAscending’ variable. CubaTreeTableWidget.java(444)
Странные возвращаемые значения функций
Предупреждение 1
V6037 An unconditional ‘return’ within a loop. QueryCacheManager.java(128)
public <T> T getSingleResultFromCache(QueryKey queryKey, List<View> views) {
....
for (Object id : queryResult.getResult()) {
return (T) em.find(metaClass.getJavaClass(), id, views.toArray(....));
}
....
}
Анализатор обнаружил безусловный вызов оператора return на первой же итерации цикла for. Возможно, тут ошибка, или нужно переписать код на использование оператора if.
Предупреждение 2
V6014 It’s odd that this method always returns one and the same value. DefaultExceptionHandler.java(40)
@Override
public boolean handle(ErrorEvent event, App app) {
Throwable t = event.getThrowable();
if (t instanceof SocketException
|| ExceptionUtils.getRootCause(t) instanceof SocketException) {
return true;
}
if (ExceptionUtils.getThrowableList(t).stream()
.anyMatch(o -> o.getClass().getName().equals("...."))) {
return true;
}
if (StringUtils.contains(ExceptionUtils.getMessage(t), "....")) {
return true;
}
AppUI ui = AppUI.getCurrent();
if (ui == null) {
return true;
}
if (t != null) {
if (app.getConnection().getSession() != null) {
showDialog(app, t);
} else {
showNotification(app, t);
}
}
return true;
}
Эта функция во всех случаях возвращает значение true. Но вот в самой последней строчке напрашивается возврат значения false. Возможно, тут допущена ошибка.
Весь список подозрительных функций с похожим кодом:
- V6014 It’s odd that this method always returns one and the same value. ErrorNodesFinder.java(31)
- V6014 It’s odd that this method always returns one and the same value. FileDownloadController.java(69)
- V6014 It’s odd that this method always returns one and the same value. IdVarSelector.java(73)
- V6014 It’s odd that this method always returns one and the same value. IdVarSelector.java(48)
- V6014 It’s odd that this method always returns one and the same value. IdVarSelector.java(67)
- V6014 It’s odd that this method always returns one and the same value. IdVarSelector.java(46)
- V6014 It’s odd that this method always returns one and the same value. JoinVariableNode.java(57)
Предупреждение 3
V6007 Expression ‘needReload’ is always false. WebAbstractTable.java(2702)
protected boolean handleSpecificVariables(Map<String, Object> variables) {
boolean needReload = false;
if (isUsePresentations() && presentations != null) {
Presentations p = getPresentations();
if (p.getCurrent() != null && p.isAutoSave(p.getCurrent())
&& needUpdatePresentation(variables)) {
Element e = p.getSettings(p.getCurrent());
saveSettings(e);
p.setSettings(p.getCurrent(), e);
}
}
return needReload;
}
Функция возвращает переменную needReload, значение которой всегда равно false. Скорее всего, в одном из условий забыли добавить код изменения значения переменной.
Предупреждение 4
V6062 Possible infinite recursion inside the ‘isFocused’ method. GwtAceEditor.java(189), GwtAceEditor.java(190)
public final native void focus() /*-{
this.focus();
}-*/;
public final boolean isFocused() {
return this.isFocused();
}
Анализатор обнаружил функцию, которая вызывается рекурсивно без условия остановки рекурсии. В этом файле много функций, которые помечены ключевым словом native и содержат закомментированный код. Скорее всего, файл в данный момент переписывается и вскоре разработчики обратят внимание и на функцию isFocused.
Разные предупреждения
Предупреждение 1
V6002 The switch statement does not cover all values of the ‘Operation’ enum: ADD. DesktopAbstractTable.java(665)
/**
* Operation which caused the datasource change.
*/
enum Operation {
REFRESH,
CLEAR,
ADD,
REMOVE,
UPDATE
}
@Override
public void setDatasource(final CollectionDatasource datasource) {
....
collectionChangeListener = e -> {
switch (e.getOperation()) {
case CLEAR:
case REFRESH:
fieldDatasources.clear();
break;
case UPDATE:
case REMOVE:
for (Object entity : e.getItems()) {
fieldDatasources.remove(entity);
}
break;
}
};
....
}
В операторе switch не рассмотрено значение ADD. Оно является единственным нерассмотренным, поэтому стоит проверить, ошибка это или нет.
Предупреждение 2
V6021 Variable ‘source’ is not used. DefaultHorizontalLayoutDropHandler.java(177)
@Override
protected void handleHTML5Drop(DragAndDropEvent event) {
LayoutBoundTransferable transferable = (LayoutBoundTransferable) event
.getTransferable();
HorizontalLayoutTargetDetails details = (HorizontalLayoutTargetDetails) event
.getTargetDetails();
AbstractOrderedLayout layout = (AbstractOrderedLayout) details
.getTarget();
Component source = event.getTransferable().getSourceComponent(); // <=
int idx = (details).getOverIndex();
HorizontalDropLocation loc = (details).getDropLocation();
if (loc == HorizontalDropLocation.CENTER
|| loc == HorizontalDropLocation.RIGHT) {
idx++;
}
Component comp = resolveComponentFromHTML5Drop(event);
if (idx >= 0) {
layout.addComponent(comp, idx);
} else {
layout.addComponent(comp);
}
if (dropAlignment != null) {
layout.setComponentAlignment(comp, dropAlignment);
}
}
В коде объявляется и не используется переменная source. Возможно, как и другую переменную comp этого же типа, source забыли добавить в layout.
Ещё функции с неиспользуемыми переменными:
- V6021 Variable ‘source’ is not used. DefaultHorizontalLayoutDropHandler.java(175)
- V6021 The value is assigned to the ‘r’ variable but is not used. ExcelExporter.java(262)
- V6021 Variable ‘over’ is not used. DefaultCssLayoutDropHandler.java(49)
- V6021 Variable ‘transferable’ is not used. DefaultHorizontalLayoutDropHandler.java(171)
- V6021 Variable ‘transferable’ is not used. DefaultHorizontalLayoutDropHandler.java(169)
- V6021 Variable ‘beanLocator’ is not used. ScreenEventMixin.java(28)
Предупреждение 3
V6054 Classes should not be compared by their name. MessageTools.java(283)
public boolean hasPropertyCaption(MetaProperty property) {
Class<?> declaringClass = property.getDeclaringClass();
if (declaringClass == null)
return false;
String caption = getPropertyCaption(property);
int i = caption.indexOf('.');
if (i > 0 && declaringClass.getSimpleName().equals(caption.substring(0, i)))
return false;
else
return true;
}
Анализатор обнаружил ситуацию, когда сравнение классов осуществляется по имени. Такое сравнение является некорректным, т.к., согласно спецификации, JVM классы имеют уникальное имя только внутри пакета. Это может стать причиной некорректного сравнения и выполнения не того кода, который планировался.
Отзыв разработчиков CUBA Platform
Безусловно, в любом крупном проекте бывают баги. Именно поэтому мы с радостью согласились на предложение команды PVS-Studio проверить наш проект. В репозиторий CUBA включены форки некоторых сторонних OSS библиотек под лицензией Apache 2 и, кажется, нам нужно уделить этому коду больше внимания, анализатор нашёл довольно много проблем в этих исходниках. Сейчас мы используем SpotBugs в качестве основного анализатора, и он не находит некоторые существенные проблемы, найденные PVS-Studio. Самое время пойти и написать дополнительные проверки самим. Большое спасибо команде PVS-Studio за проделанную работу.
Также разработчики отметили, что предупреждения V6013 и V6054 являются ложными. Код был написан таким намеренно. Статический анализатор предназначен для выявления подозрительных фрагментов кода и вероятность нахождения ошибки у всех инспекций разная. Тем не менее, работать с такими предупреждениями легко, используя удобный механизм массового подавления предупреждений анализатора без модификации файлов исходного кода.
Ещё команда PVS-Studio не может пройти мимо фразы «самое время пойти и написать дополнительные проверки самим» и не оставить эту картинку
Заключение
PVS-Studio будет отличным дополнением для вашего проекта к уже существующим инструментам улучшения качества кода. Особенно, если в штате десятки, сотни и тысячи сотрудников. PVS-Studio предназначен не только для нахождения ошибок, но и для их исправления. Причём речь не об автоматическом редактировании кода, а о надёжном контроле качества кода. В большой компании невозможно представить ситуацию, когда абсолютно все разработчики будут самостоятельно проверять свой код различными инструментами, поэтому таким компаниям больше подходят инструменты типа PVS-Studio, где предусмотрен контроль качества кода на всех этапах разработки, не только у рядового программиста.
Присылаем лучшие статьи раз в месяц
cuba-platform / restapi
Goto Github
PK
View Code? Open in Web Editor
NEW
7.0
6.0
932 KB
CUBA REST API Add-on
License: Apache License 2.0
Shell 0.02%
Groovy 6.68%
HTML 0.36%
cuba-component
rest-api
rest
restapi’s Introduction
restapi’s People
restapi’s Issues
Introduce a synchronous mode for storing REST API tokens in cluster
Description
When a new REST API token is stored, it is distributed among all nodes of the cluster. Right now, it is done asynchronously. This may cause the problem: in the cluster under a high load the token may be obtained, passed to another node, but the token was not replicated to the second node yet. Probably we should introduce a synchronous way of doing this (like is done for user sessions cuba.syncNewUserSessionReplication
).
In addition to using clusterManagerAPI.send(new TokenStoreAddAccessTokenMsg(....
we may optionally use the clusterManagerAPI.sendSync(new TokenStoreAddAccessTokenMsg(
in the ServerTokenStoreImpl.
There may be an application property cuba.syncTokenReplication
. The default value is false
. In the ServerTokenStoreImpl
all invocations of clusterManagerAPI.send
must be replaced with sendAsync
if the app property is set to true. See UserSessions.add(UserSession session)
method as an example.
Updates
The config cuba.rest.syncTokenReplication
is added, the default value is false
.
If true
, tokens will be sent to the cluster in a synchronous way.
Ability to set max results and offset for predefined rest queries
REST API: token revocation request with the wrong content type should returns the 400 Bad Request with the description
app/rest/v2/oauth/revoke request with the wrong content type returns the Internal Server Error with 500 code
The following exception appears in tomcat.log
org.springframework.web.bind.MissingServletRequestParameterException: Required String parameter 'token' is not present
at org.springframework.web.method.annotation.RequestParamMethodArgumentResolver.handleMissingValue(RequestParamMethodArgumentResolver.java:195) ~[spring-web-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:104) ~[spring-web-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121) ~[spring-web-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161) ~[spring-web-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:128) ~[spring-web-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:114) ~[spring-webmvc-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827) ~[spring-webmvc-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738) ~[spring-webmvc-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:963) ~[spring-webmvc-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897) ~[spring-webmvc-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) [spring-webmvc-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872) [spring-webmvc-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:648) [servlet-api.jar:na]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) [spring-webmvc-4.3.3.RELEASE.jar:4.3.3.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:729) [servlet-api.jar:na]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:230) [catalina.jar:8.5.9]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:8.5.9]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) [tomcat-websocket.jar:8.5.9]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) [catalina.jar:8.5.9]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:8.5.9]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:317) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:127) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:115) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169) [spring-security-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]```
---
Original issue: https://youtrack.haulmont.com/issue/PL-9936
Support batch entity commit
Description
Provide an ability to commit several entities (of the same type) with a single request
Updates
Added the ability to create multiple entities, one request.
Example:
POST : http://localhost:8080/app/rest/v2/entities/sec$Group
Request body:
[
{
"name":"Group1"
},
{
"name":"Group2"
},
{
"name":"Group3"
}
]
Swagger scheme contains errors
A topic on support forum: link
REST API does not support entities with composite primary key
Since id of such entities is passed as object it’s not possible to call some methods where id is used in path fragment
DELETE /entities/{entityName}/{entityId}
GET /entities/{entityName}/{entityId}
PUT /entities/{entityName}/{entityId}
Solution:
Represent composite key identifier as JSON string and apply base64 URL encoding.
Model:
@Entity(name = "ref$CompositeKeyEntity")
@Table(name = "REF_COMPOSITE_KEY")
public class CompositeKeyEntity extends BaseGenericIdEntity<EntityKey> {
private static final long serialVersionUID = -2538345720324624741L;
@EmbeddedId
private EntityKey id;
@Column(name = "NAME")
private String name;
}
@Embeddable
@MetaClass(name = "ref$EntityKey")
public class EntityKey extends EmbeddableEntity {
@Column(name = "TENANT")
private Integer tenant;
@Column(name = "ENTITY_ID")
private Long entityId;
}
Encoding sample:
- JSON:
{tenant : 1, entityId: 1}
- Base 64:
e3RlbmFudCA6IDEsIGVudGl0eUlkOiAxfQ==
- GET
/entities/ref$CompositeKeyEntity/e3RlbmFudCA6IDEsIGVudGl0eUlkOiAxfQ==
Original issue: https://youtrack.haulmont.com/issue/PL-9444
Introduce isNull entity search condition
Description
Introduce isNull
entity search condition for Rest API Add-on.
Updates
Created a platform-independent enum RestFilterOp
for the Rest API Add-on and added a new condition isNull
.
Example:
"filter": {
"conditions": [
{
"property": "name",
"operator": "isNull"
}
]
}
}
There is no ability to restrict specific REST end-points
Impossible to handle REST API services exception
If the service method throws an exception, it is caught and new RestAPIException is thrown (ServicesControllerManager._invokeServiceMethod). That makes impossible to handle custom exception types.
Original issue: https://youtrack.haulmont.com/issue/PL-9864
Merge sql scripts
Fix a problem with the migration of projects.
- Refresh create scripts
- Remove update scripts
«responseView» method parameter for entity create and update operations
By default, entity create and update operation must return a result that contains just the following fields:
{ "id": "<entityId>", "_entityName": "<entityName>", "_instanceName": "<intanceName>" }
If other entity fields are required in the response, then an optional «responseView» query parameter must be passed in the request URL. In the case the creates/updated entity must be reloaded with this view and returned in the response JSON.
Updates
Breaking changes
Added optional parameter «responseView» for create/update request. Now, by default, only 3 parameters are passed in the response.
{ "id": "<entityId>", "_entityName": "<entityName>", "_instanceName": "<intanceName>" }
To disable this function, you must set the config cuba.rest.responseViewEnabled
to false
.
Error creating SingleWAR
Environment
- Platform version: 7.1-SNAPSHOT
Description of the bug or enhancement
- Create singleWAR for restapi-addon
- Download singleWar to Tomcat
- Start a project
ER:
Successful start
AR:
INFO: Server initialization in [564] milliseconds ╨░╨┐╤А 18, 2019 11:23:54 AM org.apache.catalina.core.StandardService startInternal INFO: Starting service [Catalina] ╨░╨┐╤А 18, 2019 11:23:54 AM org.apache.catalina.core.StandardEngine startInternal INFO: Starting Servlet engine: [Apache Tomcat/9.0.14] ╨░╨┐╤А 18, 2019 11:23:55 AM org.apache.catalina.startup.HostConfig deployWAR INFO: Deploying web application archive [C:ProjectsAddonsrestapideploytomcatwebappsrestapi.war] 11:24:06.505 INFO c.h.cuba.core.sys.AppComponents - Using app components: [com.haulmont.cuba] 11:24:06.559 INFO c.h.c.c.s.AbstractWebAppContextLoader - Loading app properties from classpath:com/haulmont/addon/restapi/web-app.properties 11:24:06.567 INFO c.h.c.c.s.AbstractWebAppContextLoader - Loading app properties from /WEB-INF/local.app.properties 11:24:08.299 ERROR c.h.c.c.s.AbstractWebAppContextLoader - Error initializing application org.springframework.beans.factory.BeanDefinitionStoreException: Unexpected exception parsing XML document from class path resource [com/haulmont/cuba/web-spring.xml]; nested exception is org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'cuba_DataManager' for bean class [com.haulmont.cuba.core.app.DataManagerBean] conflicts with existing, non-compatible bean definition of same name and class [com.haulmont.cuba.client.sys.DataManagerClientImpl] at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:419) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:336) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:304) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:188) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:224) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:195) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:257) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:128) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:94) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:133) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:636) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:521) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:144) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:95) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE] at com.haulmont.cuba.core.sys.CubaClassPathXmlApplicationContext.<init>(CubaClassPathXmlApplicationContext.java:27) ~[cuba-global-7.1-SNAPSHOT.jar:7.1-SNAPSHOT] at com.haulmont.cuba.core.sys.AbstractAppContextLoader.createApplicationContext(AbstractAppContextLoader.java:90) ~[cuba-global-7.1-SNAPSHOT.jar:7.1-SNAPSHOT] at com.haulmont.cuba.core.sys.AbstractAppContextLoader.initAppContext(AbstractAppContextLoader.java:62) ~[cuba-global-7.1-SNAPSHOT.jar:7.1-SNAPSHOT] at com.haulmont.cuba.core.sys.AbstractWebAppContextLoader.contextInitialized(AbstractWebAppContextLoader.java:78) ~[cuba-global-7.1-SNAPSHOT.jar:7.1-SNAPSHOT] at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4663) [catalina.jar:9.0.14] at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5131) [catalina.jar:9.0.14] at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) [catalina.jar:9.0.14] at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:713) [catalina.jar:9.0.14] at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:690) [catalina.jar:9.0.14] at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:695) [catalina.jar:9.0.14] at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:978) [catalina.jar:9.0.14] at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1850) [catalina.jar:9.0.14] at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_181] at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_181] at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) [tomcat-util.jar:9.0.14] at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112) [na:1.8.0_181] at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:773) [catalina.jar:9.0.14] at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:427) [catalina.jar:9.0.14] at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1577) [catalina.jar:9.0.14] at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:309) [catalina.jar:9.0.14] at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:123) [catalina.jar:9.0.14] at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:424) [catalina.jar:9.0.14] at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:367) [catalina.jar:9.0.14] at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:934) [catalina.jar:9.0.14] at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:831) [catalina.jar:9.0.14] at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) [catalina.jar:9.0.14] at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1382) [catalina.jar:9.0.14] at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1372) [catalina.jar:9.0.14] at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_181] at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) [tomcat-util.jar:9.0.14] at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) [na:1.8.0_181] at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:907) [catalina.jar:9.0.14] at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:262) [catalina.jar:9.0.14] at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) [catalina.jar:9.0.14] at org.apache.catalina.core.StandardService.startInternal(StandardService.java:423) [catalina.jar:9.0.14] at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) [catalina.jar:9.0.14] at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:933) [catalina.jar:9.0.14] at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) [catalina.jar:9.0.14] at org.apache.catalina.startup.Catalina.start(Catalina.java:637) [catalina.jar:9.0.14] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_181] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_181] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_181] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_181] at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:350) [bootstrap.jar:9.0.14] at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:492) [bootstrap.jar:9.0.14] Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'cuba_DataManager' for bean class [com.haulmont.cuba.core.app.DataManagerBean] conflicts with existing, non-compatible bean definition of same name and class [com.haulmont.cuba.client.sys.DataManagerClientImpl] at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.checkCandidate(ClassPathBeanDefinitionScanner.java:348) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:286) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.context.annotation.ComponentScanBeanDefinitionParser.parse(ComponentScanBeanDefinitionParser.java:90) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.xml.NamespaceHandlerSupport.parse(NamespaceHandlerSupport.java:74) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1366) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1352) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:179) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:149) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:96) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:513) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:393) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE] ... 58 common frames omitted ╨░╨┐╤А 18, 2019 11:24:08 AM org.apache.catalina.core.StandardContext startInternal SEVERE: One or more listeners failed to start. Full details will be found in the appropriate container log file ╨░╨┐╤А 18, 2019 11:24:08 AM org.apache.catalina.core.StandardContext startInternal SEVERE: Context [/restapi] startup failed due to previous errors ╨░╨┐╤А 18, 2019 11:24:08 AM org.apache.catalina.startup.HostConfig deployWAR INFO: Deployment of web application archive [C:ProjectsAddonsrestapideploytomcatwebappsrestapi.war] has finished in [13┬а331] ms ╨░╨┐╤А 18, 2019 11:24:08 AM org.apache.coyote.AbstractProtocol start INFO: Starting ProtocolHandler ["http-nio-8080"] ╨░╨┐╤А 18, 2019 11:24:08 AM org.apache.coyote.AbstractProtocol start INFO: Starting ProtocolHandler ["ajp-nio-8009"] ╨░╨┐╤А 18, 2019 11:24:08 AM org.apache.catalina.startup.Catalina start INFO: Server startup in [13┬а419] milliseconds
Support base64 encoded files in REST API file upload
Support anonymousAllowed for REST queries
See the topic on support forum https://www.cuba-platform.com/discuss/t/can-set-anonymousallowed-for-a-jpql-query/9804
Updates
In the queries configuration file for the REST API added boolean attribute anonymousAllowed
.
<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-queries.xsd">
<query name="allUsers" entity="sec$User" anonymousAllowed="true">
<jpql><![CDATA[select u from sec$User u]]></jpql>
</query>
</queries>
When anonymousAllowed = "true"
the anonymous user can call this query.
WebSocket & PUSH support for REST-API controllers
Connect dependency through bom file
Update all dependencies in the gradle.build file using bom file.
Provide an easy way to customize date format for REST API
REST API: The ‘Accept-Language’ request header is ignored when middleware service is invoked
Environment
- Platform version: 6.10.9
Description of the bug or enhancement
If the middleware service is invoked with the standard rest api and the Accept-Language
header is set in the request, then it is expected that UserSession in the service implementation will have the corresponding locale. However, it doesn’t.
The problem is that REST API filter sets the locale from the Accept-Language
header to the UserInvocationContext:
UserInvocationContext.setRequestScopeInfo(sessionId, locale, null, null, null);
When the middleware service is invoked after that, this value is not used.
Forum: https://www.cuba-platform.ru/discuss/t/ignoriruetsya-accept-language-pri-poluchenii-access-tokena-s-pomoshhyu-refresh-tokena-cherez-rest-api/2926
Publish Addon Swagger
Support serialization for NULL values for REST services
RefreshToken and AccessToken entity classes are not enhanced
Environment
- Platform version: 7.1
- Addon version: 0.1-SNAPSHOT
- Client type: Web
Description of the bug or enhancement
- Minimal reproducible example
Run application on 7.1-SNAPSHOT with REST-API.
- Actual behavior
2019-04-19 11:27:38.381 ERROR [main] com.haulmont.cuba.core.sys.persistence.EclipseLinkSessionEventListener -
=================================================================
Problems with entity enhancement detected:
Entity class RefreshToken is missing some of enhancing interfaces: CubaEnhanced; PersistenceObject; PersistenceWeaved; PersistenceWeavedFetchGroups; PersistenceWeavedChangeTracking;
Entity class AccessToken is missing some of enhancing interfaces: CubaEnhanced; PersistenceObject; PersistenceWeaved; PersistenceWeavedFetchGroups; PersistenceWeavedChangeTracking;
Also, there is no entitiesEnhancing in build.gradle of the addon: https://github.com/cuba-platform/restapi/blob/master/build.gradle#L128
JSON in responses for entity create and update operations should not contain not-persistent meta-properties
JSON in responses for entity create and update operations should not contain not-persistent attributes. Requests for entity read should return not-persistent attributes.
This should be done because of the following case.
There are a Debtor
and a Case
entities. The Case
entity has a not-persistent property that relates to debtor property:
@Table(name = "DEBT_CASE") @Entity(name = "debt$Case") public class Case extends StandardEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "DEBTOR_ID") protected Debtor debtor; @MetaProperty(related = "debtor") public String getNonPersistentDebtorTitle() { return debtor == null ? null : debtor.getTitle(); } }
Suppose we create or update the Case. We accidentally passed the «nonPersistentDebtorTitle» in JSON as well. The JSON has a reference to the Debtor.
{ "number": "CASE-001", "nonPersistentDebtorTitle": "someValue", "debtor": { "id": "60885987-1b61-4247-94c7-dff348347f93" } }
In this case, after the entity is commited by the DataManager, it is serialized using the EntitySerializationAPI. By default, non-persistent meta-properties are also serialized, so a property value is read during the serialization. But since the «debtor.title» property is not read, an UnfetchedAttributeException is thrown.
IllegalArgumentException via REST API with Composition with back references and m-to-n relationship
Provide method to obtain entity search count
Description
Currently it’s possible to obtain search result count by using returnCount
parameter. However there are some cases when only count is needed.
Updates
Added a new endpoint /entities/{entityName}/search/count
for GET and POST methods.
Permission config is not registered in app-component.xml
It’s not possible to setup REST API permissions from Generic UI.
STR:
- Open Role editor
- Open Specific tab
AR:
REST API permissions are not present.
ER:
Use REST API permission should be available
REST API: Login is performed twice when a new access token is obtained
See: cuba-platform/cuba#2277
Environment
- Platform version: 6.10.10
Description of the bug
- Obtain a new REST API access token (
http://localhost:8080/app/rest/v2/oauth/token
) - In the log you’ll see that login was performed twice:
01:42:12.406 INFO c.h.c.s.a.AuthenticationManagerBean - Logged in: 24668c0b-ce4d-a14c-da21-27ff4adb7735 [admin]
01:42:16.460 INFO c.h.c.s.a.AuthenticationManagerBean - Logged in: 78e5c771-47cf-bc42-2e69-9a35d391ed28 [admin]
01:42:17.419 INFO c.h.r.auth.ClientProxyTokenStore - REST API access token stored: [9265540714] ***-9c786317e72d
The error is probably caused by the issue cuba-platform/cuba#2045
Forum topic: https://www.cuba-platform.com/discuss/t/custom-portal-rest-api-failure-after-platform-upgrade/9571
Custom REST controllers improvements
IDP Integration does not work
REST-API: not-permitted entities serialization on service invocation
STR:
-
Disable read of some entity (i.e. sec$User)
-
Provide a method which loads these entities and expose it in REST API
@Override
public List<User> getUsers() {
LoadContext<User> lc = new LoadContext<>(User.class);
lc.setQueryString("select u from sec$User u");
return dataManager.loadList(lc);
}
- Invocation of the method from REST API returns list of entities but all attributes are stripped:
[
{
"_entityName": "sec$User",
"_instanceName": "[]",
"id": "12000811-36f9-9055-42bb-d2546c46e32a"
}, {
"_entityName": "sec$User",
"_instanceName": "[]",
"id": "09f10596-832f-2873-0968-05892effd3e5"
}
]
Consider: not to strip attributes or do not return entities at all
Original issue: https://youtrack.haulmont.com/issue/PL-9272
Entity JSON returned by the REST API should display fields of nested entities that are cyclic dependencies
For example:
Notice how one of the nested books holds title but the other, which is the same book as the parent of author, does not.
[ // books { "id": "123", "_entityName": "book", "title": "Book cuba-platform/cuba#123", "author": { "id": "234", "_entityName": "author", "name": "James Person", "books": [ { // note that this does not include "title" "id": "123", "_entityName": "book" }, { // but this does "id": "456", "_entityName": "book", "title": "Book cuba-platform/cuba#456" } ] } } ]
Original issue: https://youtrack.haulmont.com/issue/PL-10398
Publish XSD schemes
Cannot set null value to dynamic attribute with the REST API
Swagger documentation is not generated correctly for overloaded REST service methods#707
In case of some REST service has few methods with the same name but with different parameters (overloaded methods) we cannot generate correct Swagger documentation due to restrictions in the Swagger 2.0 Specification (OAI/OpenAPI-Specification#182).
Will be fixed when swagger-core will be released with 2.0.0 version.
REST API ignores attribute permissions for related entities
Environment
- Platform version: 7.0.8
- Client type: Web
- Browser: Chrome, Firefox
- Database: PostgreSQL
- Operating system: Windows, Linux
Description of the bug or enhancement
- Minimal reproducible example
- Create an entity «Fruit» with attributes «description» and «size»
- Create an entity «Person» with attribute «name» of type string and another attribute «favouriteFruit» of type «Fruit»
- Create a role using standard cuba framework screens, with «description» attribute of «Fruit» set to «hide»
- Create a user and assign above role
- Create a person using POST REST API
- Query fruit directly GET REST API
-
Expected behavior
Both 5 and 6 only show «size» for the fruit entity -
Actual behavior
5 shows «description» as well, it should not due to permissions
When a new access token is obtained using the refresh token, an ‘Accept-Language’ header must be read
Create a FileDescriptor with the provided ID when a file is uploaded
Add ability to dynamically define view similarly to GraphQL for REST get requests
Currently, it is not possible to describe the graph of data to be fetched via rest API on the fly (using not predefined views on the server side). To make it possible developers could define them in the body of a get request, similarly to how it is achieved with GraphQL.
To prevent DDos, ability to use such functionality should be restrictable on the role level.
Original issue: https://youtrack.haulmont.com/issue/PL-9991
It’s impossible to disable file upload with the REST API
Missing _instanceName on entity create/update response
REST API queries should support params like in the datasource query filter
Support jMockit 1.46
Environment
- Platform version: 7.2
- Addon version: 7.2
Description of the bug or enhancement
In the platform updated jMockit
.
It is necessary to add an argument to run the tests correctly.
test {
jvmArgs "-javaagent:${classpath.find { it.name.contains("jmockit") }.absolutePath}"
}
Related: cuba-platform/cuba#2151
Ability to upload temporary file for processing in a service using REST-API
REST API: provide an option to include built-in views in method which returns entity views
Method for loading entity views does not return info about built-in views (_minimal
, _base
, _local
). We can get built-in views separately using /metadata/entities/{entityName}/views/{viewName}
but it will require many separate requests e.g. if you want to build correct deep graph for some complex view.
So it makes sense to introduce includeSystem
flag which will return built-in views along with explicitly defined.
The release must have a version corresponding to the platform version
AR:
Platform 7.1.0 — Add-on 0.1-SNAPSHOT
ER:
For example:
Platform 7.1-SNAPSHOT — Add-on 7.1-SNAPSHOT
Platform 7.1.0 — Add-on 7.1.0 (or 7.1.1, 7.1.2, etc)
Platform 7.2.3 — Add-on 7.2.2
@Future and @Past validations have wrong invalidValue in the response body in the REST API
@sukhova commented on Thu Nov 01 2018
Environment
- Platform version: 6.10, 7.0
- Operating system: Windows
Description of the bug or enhancement
- Set the @past annotation for some date field
- Try to create new instance with invalid date using REST API
- See the response body:
AR:
{
"message": "must be in the past",
"messageTemplate": "{javax.validation.constraints.Past.message}",
"path": "dateOfBirth",
"invalidValue": 1572617541047
}
ER
«invalidValue» param must have the Date type
Updates
Now «InvalidValue» returned in the format that depends on the locale and @Temporal
.
https://doc.cuba-platform.com/manual-7.1/datatype_ui_format.html
Ability to sort entities by multiple attributes
Currently in entities load/search methods it’s possible to specify the only attribute for sorting.
REST-API anonymous access tests
When trying to update a read-only entity attribute an error should be returned
When we try to update a read-only attribute the 200 (OK) status is returned, although the attribute is not actually updated. Probably we should return an error in this case.
Original issue: https://youtrack.haulmont.com/issue/PL-9968
REST healthcheck endpoint does not respect CORS settings
Entity tokens are not created when rest-addon is connected
Sql-scripts restapi are not executed when restapi-addon is connected
Recommend Projects
-
ReactA declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
TypescriptTypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlowAn Open Source Machine Learning Framework for Everyone
-
DjangoThe Web framework for perfectionists with deadlines.
-
LaravelA PHP framework for web artisans
-
D3Bring data to life with SVG, Canvas and HTML. 📊📈🎉
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
FacebookWe are working to build community through open source technology. NB: members must have two-factor auth.
-
MicrosoftOpen source projects and samples from Microsoft.
-
GoogleGoogle ❤️ Open Source for everyone.
-
AlibabaAlibaba Open Source for everyone
-
D3Data-Driven Documents codes.
-
TencentChina tencent open source team.
CLI installation guide
In this video, we’ll get an overview of CUBA command line interface. In a nutshell, CUBA CLI is a command line utility that allows you to easily create projects based on CUBA Platform. Also, it provides the lightweight scaffolding of the main project artifacts: entities, screens, services, and so on. With this tool, you can use the framework without any additional tools.
I will show you how to create a simple application for customers and orders management using only a command line and a Java IDE.
Creating a project
Open a terminal and run the cuba-cli command to start CUBA CLI.
To create a new application, input the command create-app. You can use TAB auto-completion. CLI will ask you about the project. If a question implies the default answer, you can press ENTER to accept it. You will be asked the following questions:
-
Repository to be used in the project. The list of repositories already contains binary artifacts repository URL and authentication parameters. Feel free to choose any of them.
-
Project name – for sample projects, CLI generates random names that can be selected by default.
-
Project namespace – the namespace which will be used as a prefix for entity names and database tables. The namespace can consist of Latin letters only and should be as short as possible.
-
Root package – the root package of Java classes.
-
Platform version – the platform artifacts of the selected version will be automatically downloaded from the repository on project build.
-
Database – the SQL database to use. For the sample application, choose HSQLDB.
An empty project will be created in a new folder in the current directory.
Close CLI or open a new terminal window and navigate to the project folder.
Run gradlew assemble. At this stage, all the required libraries will be downloaded, and CLI will build project artifacts in the build subfolders of the project folder.
Run gradlew startDb to start the local HyperSQL server.
Run gradlew createDb to create the database instance.
Run gradlew setupTomcat deploy start. The Tomcat server with the application will be deployed in the deploy subfolder of the project.
Open the web browser and proceed to the default application URL (http://localhost:8080/app). The username and password are admin — admin, they are filled in by default.
The running out-of-the-box application contains two main menu items: Administration and Help, as well as the functionality for security and administration.
To stop the application, run gradlew stop.
CUBA plugin for IntelliJ IDEA
In order to develop business logic, you can use any Java IDE. To make it easier, I will use the free version of dedicated CUBA Studio IDE. It is based on the open-source IntelliJ IDEA Platform and extends its functionality with some CUBA-specific features: database scripts generation, navigation between screen descriptor, controller and data model components, advanced injections, CUBA-specific code generation and refactoring, and so on. Some part of these tasks can also be done in CLI.
Import the project into IDE as described in the documentation and wait for Gradle synchronization and project indexing process to complete. The CUBA project tree should appear in the Project tool window.
Creating the data model
Let’s create a data model and a graphic interface for our simple CUBA-based application for customers and orders management. In our data model, the Order entity should have a many-to-one relationship to Customer.
Let’s start with creating a Customer class. Enter create-entity in a terminal. You will be asked to enter the entity class name — Customer — and select the entity class package name. Accept default.
An entity may be Persistent, Persistent Embedded or Not Persistent. Choose Persistent to make the entity able to be saved in the database.
Let’s add some fields. Open the created class in Studio. It has appeared in the Data Model section of the project tree. Add the name and email fields, both of String type, and create getters and setters for them. CUBA platform supports localization in the same way as other Java applications: by using message.properties files. Let’s add messages for the new attributes. We will also set the name pattern to get a meaningful Customer instance representation in UI. CUBA CLI generates DB scripts stubs for the created entities. To add the custom fields, I will use the Generate Database Scripts command in the main menu. If the new script conflicts with or overlays the older one, the older one will be marked to be deleted. Of course, you can manage the database script files and add fields manually without Studio.
You can run the gradlew updateDb task either from Studio or command line interface.
Creating the UI
Now let’s create the user interface for the application. CUBA CLI can generate the standard CRUD screens using the predefined screen templates. For example, the browser is a screen for viewing all records from a database, it includes data filter and supports paging by default. The editor is a screen for viewing or editing an individual record.
Enter create-screen, select new browse screen_,_ select the Customerentity and accept the defaults. Create the Edit screen for the Customer entity in the same way.
The screens have been created in the web module package. They are now available in the Generic UI section of the project tree. Let’s customize them a little, adding name and email fields to the browse table and editor form. Use the Studio popups and intentions to speed up the process.
Enhancing the data model
Let’s create the Order entity. Feel free to use TAB auto-completion in CLI. Add the following attributes: date — the order date, amount — the order amount of BigDecimal type, and customer — the many-to-one reference to the Customer entity. Don’t forget to create getters and setters and define the messages.
Now let’s add two more entities: OrderLine and Product, so the Order will contain a set of order lines which refer to some product. Furthermore, we will implement the order amount calculation based on the quantity and price of the selected products.
So, we create the Product entity and define its attributes: name which is a String and price which is a BigDecimal. Select name as the Product’s name pattern.
Now let’s create the OrderLine entity. Add the following attributes: order which is a many-to-one reference to Order; product which is a many-to-one reference to Product; finally, the quantity attribute of BigDecimal type which sets the product quantity for a given order line.
Add the collection of lines for the order: create the lines attribute which is a Composition reference to the OrderLine entity. That means that the order lines are created and edited alongside the original order. The corresponding attribute on the reverse side of the relationship is defined with the mappedBy attribute.
The constraints for the database integrity will be created on the next scripts generation.
Enhancing the UI
Now. let’s scaffold the user interface for the new entities. At first, create the standard screens for Product.
After that, create the UI for OrderLine. The list of order lines should be displayed only in the Order editor, so we don’t need an OrderLine browser.
Since the OrderLine entity includes the reference attribute product, we have to create an entity view that includes this attribute. A view, in CUBA terms, is a declarative way to specify which data (attributes and nested instances/collections) should be extracted from a database.
Now we can display products in the OrderLine editor.
Let’s create the standard screens for the Order. The Order entity contains two reference attributes — customer and lines. Suppose we would like to display customers in the browse screen, and the complete graph of entities in the Order editor. In this case, we need two separate views.
Now we can add all the necessary fields to Order screens. To display the list of customers in the lookup field, create a new data container that will load the collection of customers from the database. Let’s also add the table for displaying the order lines.
We will also make the Customer editor display the list of orders. Open the customer-edit.xml file and create a new data container for loading the linked Order instances. Pass the edited Customer instance as a parameter for the container’s loader.
All the screens are done now. Start the application server and make sure they are functional.
Development beyond CRUD
We still have to implement the order amount calculations based on the product price and quantity. Switch to the Java controller of the Order editor. To begin with, let’s inject the link to the linesDc data container into the controller class. Now subscribe to the CollectionChangeEvent that will call the new calculateAmount() method every time the lines collection is changed.
The changes in the existing screens will be hot deployed, and we will see the changes simply by switching to the web browser and reopening the order editor, without stopping the application server.
As we can see, the order amount corresponds to the order lines, and if you modify them, the amount is recalculated.
Our application is done, at a first approximation. Feel free to develop it using any tools you like. Thank you for your attention.