Настройка клиента Stomp в android с Spring framework на стороне сервера

Я разрабатываю приложение для android, которое обменивается данными с сервером причала, настроенным весной. Чтобы получить более динамичное приложение для android, я пытаюсь использовать протокол WebSocket с сообщениями Stomp.

чтобы реализовать этот материал, я настраиваю брокера сообщений веб-сокета весной:

@Configuration
//@EnableScheduling
@ComponentScan(
        basePackages="project.web",
        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, value = Configuration.class)
)
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/message");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/client");
    }
}

и a SimpMessageSendingOperations весной контроллер для отправки сообщения с сервера на клиент:

@Controller
public class MessageAddController {
    private final Log log = LogFactory.getLog(MessageAddController.class);

    private SimpMessageSendingOperations messagingTemplate;

    private UserManager userManager;

    private MessageManager messageManager;

    @Autowired
    public MessageAddController(SimpMessageSendingOperations messagingTemplate, 
            UserManager userManager, MessageManager messageManager){
        this.messagingTemplate = messagingTemplate;
        this.userManager = userManager;
        this.messageManager = messageManager;
    }

    @RequestMapping("/Message/Add")
    @ResponseBody
    public SimpleMessage addFriendship(
            @RequestParam String content,
            @RequestParam Long otherUser_id
    ){
        if(log.isInfoEnabled())
            log.info("Execute MessageAdd action");
        SimpleMessage simpleMessage;

        try{
            User curentUser = userManager.getCurrentUser();
            User otherUser = userManager.findUser(otherUser_id);

            Message message = new Message();
            message.setContent(content);
            message.setUserSender(curentUser);
            message.setUserReceiver(otherUser);

            messageManager.createMessage(message);          
            Message newMessage = messageManager.findLastMessageCreated();

            messagingTemplate.convertAndSend( 
                    "/message/add", newMessage);//send message through websocket

            simpleMessage = new SimpleMessage(null, newMessage);
        } catch (Exception e) {
            if(log.isErrorEnabled())
                log.error("A problem of type : " + e.getClass() 
                        + " has occured, with message : " + e.getMessage());
            simpleMessage = new SimpleMessage(
                            new SimpleException(e.getClass(), e.getMessage()), null);
        }
        return simpleMessage;
    }
}

когда я тестирую эту конфигурацию в веб-браузере с помощью топать.js, у меня нет никаких проблем : сообщения отлично обмениваются между веб-браузером и сервером причала. Код JavaScript, используемый для теста веб-браузера:

    var stompClient = null;

    function setConnected(connected) {
        document.getElementById('connect').disabled = connected;
        document.getElementById('disconnect').disabled = !connected;
        document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
        document.getElementById('response').innerHTML = '';
    }

    function connect() {
        stompClient = Stomp.client("ws://YOUR_IP/client");         
        stompClient.connect({}, function(frame) {
            setConnected(true);
            stompClient.subscribe('/message/add', function(message){
                showMessage(JSON.parse(message.body).content);
            });
        });
    }

    function disconnect() {
        stompClient.disconnect();
        setConnected(false);
        console.log("Disconnected");
    }


    function showMessage(message) {
        var response = document.getElementById('response');
        var p = document.createElement('p');
        p.style.wordWrap = 'break-word';
        p.appendChild(document.createTextNode(message));
        response.appendChild(p);
    }

проблемы возникают, когда я пытаюсь использовать stomp в Android с библиотеками, такими как gozirra, activemq-stomp или другие : в большинстве случаев соединение с сервером не работает. Мое приложение перестает работать, и через несколько минут у меня есть следующее сообщение в logcat:java.net.UnknownHostException: Unable to resolve host "ws://192.168.1.39/client": No address associated with hostname и я не понимаю, почему. Код, использующий библиотеку Gozzira, которая управляет обращением stomp в моей деятельности android:

private void stomp_test() {
    String ip = "ws://192.172.6.39/client";
    int port = 8080;

    String channel = "/message/add";
    Client c;

    try {
        c = new Client( ip, port, "", "" );
        Log.i("Stomp", "Connection established");
        c.subscribe( channel, new Listener() {
            public void message( Map header, String message ) {
                Log.i("Stomp", "Message received!!!");
              }
        });

    } catch (IOException ex) {
        Log.e("Stomp", ex.getMessage());
        ex.printStackTrace();

    } catch (LoginException ex) {
        Log.e("Stomp", ex.getMessage());
        ex.printStackTrace();
    } catch (Exception ex) {
        Log.e("Stomp", ex.getMessage());
        ex.printStackTrace();
    }

}

после некоторых исследований я обнаружил, что большинство людей, которые хотят использовать stomp over websocket с клиентом Java, используют сервер ActiveMQ, как в этом сайт. Но spring tools очень просты в использовании, и было бы здорово, если бы я мог сохранить свой серверный слой, как сейчас. Кто-то знает, как использовать stomp java (Android) на стороне клиента с конфигурацией Spring на стороне сервера?

3 ответов


моя реализация протокола STOMP для android (или простой java) с RxJava https://github.com/NaikSoftware/StompProtocolAndroid. Протестировано на сервере STOMP с SpringBoot. Простой пример (с retrolambda):

private StompClient mStompClient;

 // ...

 mStompClient = Stomp.over(WebSocket.class, "ws://localhost:8080/app/hello/websocket");
 mStompClient.connect();

 mStompClient.topic("/topic/greetings").subscribe(topicMessage -> {
     Log.d(TAG, topicMessage.getPayload());
 });

 mStompClient.send("/app/hello", "My first STOMP message!");

 // ...

 mStompClient.disconnect();

добавьте следующий путь к классам в project:

  classpath 'me.tatarka:gradle-retrolambda:3.2.0'

добавьте следующую вещь в сборку приложения.Gradle в :

apply plugin: 'me.tatarka.retrolambda'

android {
       .............
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}


  dependencies {
      ............................
    compile 'org.java-websocket:Java-WebSocket:1.3.0'
    compile 'com.github.NaikSoftware:StompProtocolAndroid:1.1.5'
}

все работает асинхронно! Вы можете позвонить connect() после subscribe() и send(), сообщения будут помещены в очередь.

дополнительные характеристики:

  • дополнительные заголовки HTTP для запроса рукопожатия (для передачи токена аутентификации или другого)
  • вы можете реализовать собственный транспорт для библиотеки, просто реализовать интерфейс ConnectionProvider
  • подписаться на события жизненного цикла соединения (подключено, закрыто, ошибка)

Например :

public class MainActivity extends AppCompatActivity {
    private StompClient mStompClient;
    public static  final String TAG="StompClient";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button view = (Button) findViewById(R.id.button);
        view.setOnClickListener(e->  new LongOperation().execute(""));


    }


        private class LongOperation extends AsyncTask<String, Void, String> {
        private StompClient mStompClient;
        String TAG="LongOperation";
        @Override
        protected String doInBackground(String... params) {

            mStompClient = Stomp.over(WebSocket.class, "ws://localhost:8080/app/hello/websocket");
            mStompClient.connect();

            mStompClient.topic("/topic/greetings").subscribe(topicMessage -> {
                Log.d(TAG, topicMessage.getPayload());
            });

            mStompClient.send("/app/hello", "My first STOMP message!").subscribe();


            mStompClient.lifecycle().subscribe(lifecycleEvent -> {
                switch (lifecycleEvent.getType()) {

                    case OPENED:
                        Log.d(TAG, "Stomp connection opened");
                        break;

                    case ERROR:
                        Log.e(TAG, "Error", lifecycleEvent.getException());
                        break;

                    case CLOSED:
                        Log.d(TAG, "Stomp connection closed");
                        break;
                }
            });
            return "Executed";
        }

        @Override
        protected void onPostExecute(String result) {

        }

    }
}

добавить разрешение в интернет декларация.в XML

<uses-permission android:name="android.permission.INTERNET" />

Я достигаю использования stomp через веб-сокет с Android и spring server.

для этого я использовал библиотеку веб-сокетов: werbench (следуйте этому ссылке чтобы скачать ее). Для установки я использовал команду maven mvn install и я вернул банку в свой локальный репозиторий. Затем мне нужно добавить слой stomp на базовый веб-сокет, но я не мог найти библиотеку stomp на java, которая могла бы управлять stomp через веб-сокет (мне пришлось отказаться от gozzira). Поэтому я создаю свой собственный. один (с топотом.Яш, как модель). Не стесняйтесь спрашивать меня, хотите ли вы взглянуть на него, но я понял это очень быстро, поэтому он не может управлять столько, сколько топать.js. Затем мне нужно реализовать аутентификацию с моим сервером spring. Чтобы добиться этого, я следовал указаниям этот сайт. когда я получаю обратно файл cookie JSESSIONID, мне просто нужно было объявить заголовок с этим файлом cookie в экземпляре веб-сокета werbench в моей "библиотеке"stomp.

изменить : это основной класс в этой библиотеке, тот, который управляет подключением к веб-сокету:

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import android.util.Log;
import de.roderick.weberknecht.WebSocket;
import de.roderick.weberknecht.WebSocketEventHandler;
import de.roderick.weberknecht.WebSocketMessage;

public class Stomp {

    private static final String TAG = Stomp.class.getSimpleName();

    public static final int CONNECTED = 1;//Connection completely established
    public static final int NOT_AGAIN_CONNECTED = 2;//Connection process is ongoing
    public static final int DECONNECTED_FROM_OTHER = 3;//Error, no more internet connection, etc.
    public static final int DECONNECTED_FROM_APP = 4;//application explicitely ask for shut down the connection 

    private static final String PREFIX_ID_SUBSCIPTION = "sub-";
    private static final String ACCEPT_VERSION_NAME = "accept-version";
    private static final String ACCEPT_VERSION = "1.1,1.0";
    private static final String COMMAND_CONNECT = "CONNECT";
    private static final String COMMAND_CONNECTED = "CONNECTED";
    private static final String COMMAND_MESSAGE = "MESSAGE";
    private static final String COMMAND_RECEIPT = "RECEIPT";
    private static final String COMMAND_ERROR = "ERROR";
    private static final String COMMAND_DISCONNECT = "DISCONNECT";
    private static final String COMMAND_SEND = "SEND";
    private static final String COMMAND_SUBSCRIBE = "SUBSCRIBE";
    private static final String COMMAND_UNSUBSCRIBE = "UNSUBSCRIBE";
    private static final String SUBSCRIPTION_ID = "id";
    private static final String SUBSCRIPTION_DESTINATION = "destination";
    private static final String SUBSCRIPTION_SUBSCRIPTION = "subscription";


    private static final Set<String> VERSIONS = new HashSet<String>();
    static {
        VERSIONS.add("V1.0");
        VERSIONS.add("V1.1");
        VERSIONS.add("V1.2");
    }

    private WebSocket websocket;

    private int counter;

    private int connection;

    private Map<String, String> headers;

    private int maxWebSocketFrameSize;

    private Map<String, Subscription> subscriptions;

    private ListenerWSNetwork networkListener;

    /**
     * Constructor of a stomp object. Only url used to set up a connection with a server can be instantiate
     * 
     * @param url
     *      the url of the server to connect with
     */
    public Stomp(String url, Map<String,String> headersSetup, ListenerWSNetwork stompStates){       
        try {
            this.websocket = new WebSocket(new URI(url), null, headersSetup);
            this.counter = 0;

            this.headers = new HashMap<String, String>();
            this.maxWebSocketFrameSize = 16 * 1024;
            this.connection = NOT_AGAIN_CONNECTED;
            this.networkListener = stompStates;
            this.networkListener.onState(NOT_AGAIN_CONNECTED);
            this.subscriptions = new HashMap<String, Subscription>();

            this.websocket.setEventHandler(new WebSocketEventHandler() {        
                @Override
                public void onOpen(){
                    if(Stomp.this.headers != null){                                         
                        Stomp.this.headers.put(ACCEPT_VERSION_NAME, ACCEPT_VERSION);

                        transmit(COMMAND_CONNECT, Stomp.this.headers, null);

                        Log.d(TAG, "...Web Socket Openned");
                    }
                }

                @Override
                public void onMessage(WebSocketMessage message) {
                    Log.d(TAG, "<<< " + message.getText());
                    Frame frame = Frame.fromString(message.getText());
                    boolean isMessageConnected = false;

                    if(frame.getCommand().equals(COMMAND_CONNECTED)){
                        Stomp.this.connection = CONNECTED;
                        Stomp.this.networkListener.onState(CONNECTED);

                        Log.d(TAG, "connected to server : " + frame.getHeaders().get("server"));
                        isMessageConnected = true;

                    } else if(frame.getCommand().equals(COMMAND_MESSAGE)){
                        String subscription = frame.getHeaders().get(SUBSCRIPTION_SUBSCRIPTION);
                        ListenerSubscription onReceive = Stomp.this.subscriptions.get(subscription).getCallback();

                        if(onReceive != null){
                            onReceive.onMessage(frame.getHeaders(), frame.getBody());
                        } else{
                            Log.e(TAG, "Error : Subscription with id = " + subscription + " had not been subscribed");
                            //ACTION TO DETERMINE TO MANAGE SUBCRIPTION ERROR
                        }

                    } else if(frame.getCommand().equals(COMMAND_RECEIPT)){
                        //I DON'T KNOW WHAT A RECEIPT STOMP MESSAGE IS

                    } else if(frame.getCommand().equals(COMMAND_ERROR)){
                        Log.e(TAG, "Error : Headers = " + frame.getHeaders() + ", Body = " + frame.getBody());
                        //ACTION TO DETERMINE TO MANAGE ERROR MESSAGE

                    } else {

                    }

                    if(isMessageConnected)
                        Stomp.this.subscribe();
                }

                @Override
                public void onClose(){
                    if(connection == DECONNECTED_FROM_APP){
                        Log.d(TAG, "Web Socket disconnected");
                        disconnectFromApp();
                    } else{
                        Log.w(TAG, "Problem : Web Socket disconnected whereas Stomp disconnect method has never "
                                + "been called.");
                        disconnectFromServer();
                    }
                }

                @Override
                public void onPing() {

                }

                @Override
                public void onPong() {

                }

                @Override
                public void onError(IOException e) {
                    Log.e(TAG, "Error : " + e.getMessage());                
                }
            });
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    /**
     * Send a message to server thanks to websocket
     * 
     * @param command
     *      one of a frame property, see {@link Frame} for more details
     * @param headers
     *      one of a frame property, see {@link Frame} for more details
     * @param body
     *      one of a frame property, see {@link Frame} for more details
     */
    private void transmit(String command, Map<String, String> headers, String body){
        String out = Frame.marshall(command, headers, body);
        Log.d(TAG, ">>> " + out);
        while (true) {
            if (out.length() > this.maxWebSocketFrameSize) {
                this.websocket.send(out.substring(0, this.maxWebSocketFrameSize));
                out = out.substring(this.maxWebSocketFrameSize);
            } else {
                this.websocket.send(out);
                break;
            }
        }
    }

    /**
     * Set up a web socket connection with a server
     */
    public void connect(){
        if(this.connection != CONNECTED){
            Log.d(TAG, "Opening Web Socket...");
            try{
                this.websocket.connect();
            } catch (Exception e){
                Log.w(TAG, "Impossible to establish a connection : " + e.getClass() + ":" + e.getMessage());
            }
        }
    }

    /**
     * disconnection come from the server, without any intervention of client side. Operations order is very important
     */
    private void disconnectFromServer(){
        if(this.connection == CONNECTED){
            this.connection = DECONNECTED_FROM_OTHER;
            this.websocket.close();
            this.networkListener.onState(this.connection);
        }
    }

    /**
     * disconnection come from the app, because the public method disconnect was called
     */
    private void disconnectFromApp(){
        if(this.connection == DECONNECTED_FROM_APP){
            this.websocket.close();
            this.networkListener.onState(this.connection);
        }
    }

    /**
     * Close the web socket connection with the server. Operations order is very important
     */
    public void disconnect(){
        if(this.connection == CONNECTED){
            this.connection = DECONNECTED_FROM_APP;
            transmit(COMMAND_DISCONNECT, null, null);
        }
    }

    /**
     * Send a simple message to the server thanks to the body parameter
     * 
     * 
     * @param destination
     *      The destination through a Stomp message will be send to the server
     * @param headers
     *      headers of the message
     * @param body
     *      body of a message
     */
    public void send(String destination, Map<String,String> headers, String body){
        if(this.connection == CONNECTED){
            if(headers == null)
                headers = new HashMap<String, String>();

            if(body == null)
                body = "";

            headers.put(SUBSCRIPTION_DESTINATION, destination);

            transmit(COMMAND_SEND, headers, body);
        }
    }

    /**
     * Allow a client to send a subscription message to the server independently of the initialization of the web socket.
     * If connection have not been already done, just save the subscription
     * 
     * @param subscription
     *      a subscription object
     */
    public void subscribe(Subscription subscription){
        subscription.setId(PREFIX_ID_SUBSCIPTION + this.counter++);
        this.subscriptions.put(subscription.getId(), subscription);

        if(this.connection == CONNECTED){   
            Map<String, String> headers = new HashMap<String, String>();            
            headers.put(SUBSCRIPTION_ID, subscription.getId());
            headers.put(SUBSCRIPTION_DESTINATION, subscription.getDestination());

            subscribe(headers);
        }
    }

    /**
     * Subscribe to a Stomp channel, through messages will be send and received. A message send from a determine channel
     * can not be receive in an another.
     *
     */
    private void subscribe(){
        if(this.connection == CONNECTED){
            for(Subscription subscription : this.subscriptions.values()){
                Map<String, String> headers = new HashMap<String, String>();            
                headers.put(SUBSCRIPTION_ID, subscription.getId());
                headers.put(SUBSCRIPTION_DESTINATION, subscription.getDestination());

                subscribe(headers);
            }
        }
    }

    /**
     * Send the subscribe to the server with an header
     * @param headers
     *      header of a subscribe STOMP message
     */
    private void subscribe(Map<String, String> headers){
        transmit(COMMAND_SUBSCRIBE, headers, null);
    }

    /**
     * Destroy a subscription with its id
     * 
     * @param id
     *      the id of the subscription. This id is automatically setting up in the subscribe method
     */
    public void unsubscribe(String id){
        if(this.connection == CONNECTED){
            Map<String, String> headers = new HashMap<String, String>();
            headers.put(SUBSCRIPTION_ID, id);

            this.subscriptions.remove(id);
            this.transmit(COMMAND_UNSUBSCRIBE, headers, null);
        }
    }
}

Это кадр сообщения Stomp:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Frame {
//  private final static String CONTENT_LENGTH = "content-length";

    private String command;
    private Map<String, String> headers;
    private String body;

    /**
     * Constructor of a Frame object. All parameters of a frame can be instantiate
     * 
     * @param command
     * @param headers
     * @param body
     */
    public Frame(String command, Map<String, String> headers, String body){
        this.command = command;     
        this.headers = headers != null ? headers : new HashMap<String, String>();
        this.body = body != null ? body : "";
    }

    public String getCommand(){
        return command;
    }

    public Map<String, String> getHeaders(){
        return headers;
    }

    public String getBody(){
        return body;
    }

    /**
     * Transform a frame object into a String. This method is copied on the objective C one, in the MMPReactiveStompClient
     * library
     * @return a frame object convert in a String
     */
    private String toStringg(){
        String strLines = this.command;
        strLines += Byte.LF;
        for(String key : this.headers.keySet()){
            strLines += key + ":" + this.headers.get(key);
            strLines += Byte.LF;
        }
        strLines += Byte.LF;
        strLines += this.body;
        strLines += Byte.NULL;

        return strLines;
    }

    /**
     * Create a frame from a received message. This method is copied on the objective C one, in the MMPReactiveStompClient
     * library
     * 
     * @param data
     *  a part of the message received from network, which represented a frame
     * @return
     *  An object frame
     */
    public static Frame fromString(String data){
        List<String> contents = new ArrayList<String>(Arrays.asList(data.split(Byte.LF)));

        while(contents.size() > 0 && contents.get(0).equals("")){
            contents.remove(0);
        }

        String command = contents.get(0);
        Map<String, String> headers = new HashMap<String, String>();
        String body = "";

        contents.remove(0);
        boolean hasHeaders = false;
        for(String line : contents){
            if(hasHeaders){
                for(int i=0; i < line.length(); i++){
                    Character c = line.charAt(i);
                    if(!c.equals(''))
                        body += c;
                }
            } else{
                if(line.equals("")){
                    hasHeaders = true;
                } else {
                    String[] header = line.split(":");
                    headers.put(header[0], header[1]);
                }
            }
        }
        return new Frame(command, headers, body);   
    }

//    No need this method, a single frame will be always be send because body of the message will never be excessive
//    /**
//     * Transform a message received from server in a Set of objects, named frame, manageable by java
//     * 
//     * @param datas
//     *        message received from network
//     * @return
//     *        a Set of Frame
//     */
//    public static Set<Frame> unmarshall(String datas){
//      String data;
//      String[] ref = datas.split(Byte.NULL + Byte.LF + "*");//NEED TO VERIFY THIS PARAMETER
//      Set<Frame> results = new HashSet<Frame>();
//      
//      for (int i = 0, len = ref.length; i < len; i++) {
//            data = ref[i];
//            
//            if ((data != null ? data.length() : 0) > 0){
//              results.add(unmarshallSingle(data));//"unmarshallSingle" is the old name method for "fromString"
//            }
//        }         
//      return results;
//    }

    /**
     * Create a frame with based fame component and convert them into a string
     * 
     * @param command
     * @param headers
     * @param body
     * @return  a frame object convert in a String, thanks to <code>toStringg()</code> method
     */
    public static String marshall(String command, Map<String, String> headers, String body){
        Frame frame = new Frame(command, headers, body);
        return frame.toStringg();
    }

    private class Byte {
        public static final String LF = "\n";
        public static final String NULL = "";
    }
}

этот объект используется для создания подписки по протоколу stomp:

public class Subscription {

    private String id;

    private String destination;

    private ListenerSubscription callback;

    public Subscription(String destination, ListenerSubscription callback){
        this.destination = destination;
        this.callback = callback;
    }

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }

    public String getDestination() {
        return destination;
    }

    public ListenerSubscription getCallback() {
        return callback;
    }
}

по крайней мере, есть два интерфейса, используемые в качестве класса java "Run", для прослушивания сети веб-сокетов и заданного канала подписки

public interface ListenerWSNetwork {
    public void onState(int state);
}

import java.util.Map;
public interface ListenerSubscription {
    public void onMessage(Map<String, String> headers, String body);
}

для Больше информации, не смутитесь спросить мне.


идеальное решение Eperrin спасибо. Я хотел бы заполнить полное решение, например, на этапе вашей деятельности/обслуживания, который вы называете connection метод, конечно, не в MainThread.

private void connection() {
        Map<String,String> headersSetup = new HashMap<String,String>();
        Stomp stomp = new Stomp(hostUrl, headersSetup, new ListenerWSNetwork() {
            @Override
            public void onState(int state) {
            }
        });
        stomp.connect();
        stomp.subscribe(new Subscription(testUrl, new ListenerSubscription() {
            @Override
            public void onMessage(Map<String, String> headers, String body) {
            }
        }));
    }

и будьте осторожны в websocket weberknecht библиотека ошибка в класс WebSocketHandshake методом verifyServerHandshakeHeaders находится на линии 124 только проверка если(!заголовки.вам("соединение")."равно" ("обновить")) и когда сервер отправить обновление вместо обновление вы получаете ошибку ошибка подключения: отсутствует поле заголовка в рукопожатии сервера: подключение вы должны отключить игнорировать случаи если(!заголовки.вам("соединение").equalsIgnoreCase("обновить"))