This commit is contained in:
parent
58dbc3a241
commit
e730bf16b8
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,3 +5,4 @@
|
|||||||
.smart-connections
|
.smart-connections
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.idea/
|
@ -6,7 +6,7 @@ aliases:
|
|||||||
- сборки мусора
|
- сборки мусора
|
||||||
- сборщику мусора
|
- сборщику мусора
|
||||||
tags:
|
tags:
|
||||||
- зрелость/🌿
|
- maturity/🌿
|
||||||
zero-link:
|
zero-link:
|
||||||
- "[[../../../meta/zero/00 Java разработка|00 Java разработка]]"
|
- "[[../../../meta/zero/00 Java разработка|00 Java разработка]]"
|
||||||
parents:
|
parents:
|
||||||
|
36
dev/snippet/Mock конфигурация Oauth2 для SpringBoot.md
Normal file
36
dev/snippet/Mock конфигурация Oauth2 для SpringBoot.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- maturity/🌱
|
||||||
|
date:
|
||||||
|
- - 2023-11-20
|
||||||
|
zero-link:
|
||||||
|
- "[[../../../../../garden/ru/meta/zero/00 Снипеты для Java|00 Снипеты для Java]]"
|
||||||
|
parents:
|
||||||
|
linked:
|
||||||
|
- "[[../../../../../garden/ru/meta/zero/00 SpringBoot|00 SpringBoot]]"
|
||||||
|
article: https://note.struchkov.dev/mock-konfighuratsiia-dlia-oauth2-springboot/
|
||||||
|
---
|
||||||
|
Конфигурация для подключения к мок-сервису авторизации по auth2. Полезно при локальной разработке, чтобы не цепляться к настоящему Oauth2 серверу.
|
||||||
|
|
||||||
|
```yml
|
||||||
|
spring:
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
client:
|
||||||
|
registration:
|
||||||
|
mocklab:
|
||||||
|
provider: mocklab
|
||||||
|
client-authentication-method: basic
|
||||||
|
authorization-grant-type: authorization_code
|
||||||
|
scope: profile, email
|
||||||
|
redirect-uri: http://localhost:8080/login/oauth2/code/
|
||||||
|
clientId: mocklab_oidc
|
||||||
|
clientSecret: whatever
|
||||||
|
provider:
|
||||||
|
mocklab:
|
||||||
|
authorization-uri: https://oauth.mocklab.io/oauth/authorize
|
||||||
|
token-uri: https://oauth.mocklab.io/oauth/token
|
||||||
|
user-info-uri: https://oauth.mocklab.io/userinfo
|
||||||
|
user-name-attribute: sub
|
||||||
|
jwk-set-uri: https://oauth.mocklab.io/.well-known/jwks.json
|
||||||
|
```
|
111
dev/snippet/Игнорирование ошибок сертификата OkHttp3.md
Normal file
111
dev/snippet/Игнорирование ошибок сертификата OkHttp3.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- maturity/🌱
|
||||||
|
date:
|
||||||
|
- - 2023-11-20
|
||||||
|
zero-link:
|
||||||
|
- "[[../../../../garden/ru/meta/zero/00 Снипеты для Java|00 Снипеты для Java]]"
|
||||||
|
parents:
|
||||||
|
linked:
|
||||||
|
article: https://note.struchkov.dev/okhttp3-ignore-ssl/
|
||||||
|
---
|
||||||
|
|
||||||
|
> [!DANGER]
|
||||||
|
> Так лучше не делать, но иногда нужно проигнорировать ошибки связанные с сертификатом сайта.
|
||||||
|
|
||||||
|
```java
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author upagge 03.02.2021
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@UtilityClass
|
||||||
|
public class OkHttpUtil {
|
||||||
|
|
||||||
|
private static OkHttpClient client = new OkHttpClient.Builder().build();
|
||||||
|
|
||||||
|
public static OkHttpClient getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void init(boolean ignoreCertificate) {
|
||||||
|
OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||||
|
log.info("Initialising httpUtil with default configuration");
|
||||||
|
if (ignoreCertificate) {
|
||||||
|
builder = configureToIgnoreCertificate(builder);
|
||||||
|
}
|
||||||
|
//Other application specific configuration
|
||||||
|
|
||||||
|
client = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Setting testMode configuration. If set as testMode, the connection will skip certification check
|
||||||
|
private static OkHttpClient.Builder configureToIgnoreCertificate(OkHttpClient.Builder builder) {
|
||||||
|
log.warn("Ignore Ssl Certificate");
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Create a trust manager that does not validate certificate chains
|
||||||
|
final TrustManager[] trustAllCerts = new TrustManager[]{
|
||||||
|
new X509TrustManager() {
|
||||||
|
@Override
|
||||||
|
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return new java.security.cert.X509Certificate[]{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final SSLContext sslContext = SSLContext.getInstance("SSL");
|
||||||
|
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
|
||||||
|
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
|
||||||
|
|
||||||
|
builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
|
||||||
|
builder.hostnameVerifier((hostname, session) -> true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Exception while configuring IgnoreSslCertificate" + e, e);
|
||||||
|
}
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Как это использовать?
|
||||||
|
Перед получением `OkHttpClient` необходимо инициализировать настройки игнорирования сертификатов. Для этого вызываем метод:
|
||||||
|
|
||||||
|
```java
|
||||||
|
OkHttpUtil.init(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого можете получить `OkHttpClient`:
|
||||||
|
```java
|
||||||
|
OkHttpUtil.getClient();
|
||||||
|
```
|
||||||
|
|
||||||
|
Или вот так:
|
||||||
|
```java
|
||||||
|
public class HttpParse {
|
||||||
|
|
||||||
|
static {
|
||||||
|
OkHttpUtil.init(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final OkHttpClient client = OkHttpUtil.getClient();
|
||||||
|
|
||||||
|
// ... ... ... ... ...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
25
dev/snippet/Парсинг URL c помощью регулярки.md
Normal file
25
dev/snippet/Парсинг URL c помощью регулярки.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- maturity/🌱
|
||||||
|
date:
|
||||||
|
- - 2023-11-20
|
||||||
|
zero-link:
|
||||||
|
- "[[../../../../garden/ru/meta/zero/00 Снипеты для Java|00 Снипеты для Java]]"
|
||||||
|
parents: []
|
||||||
|
linked:
|
||||||
|
article: https://note.struchkov.dev/parsingh-url-c-pomoshchiu-rieghuliarki/
|
||||||
|
---
|
||||||
|
Разделение URL-адреса на протокол, домен, порт и URI с помощью регулярного выражения.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Split URL into protocol, domain, port and URI
|
||||||
|
Pattern pattern = Pattern.compile("(https?://)([^:^/]*)(:\\d*)?(.*)?");
|
||||||
|
Matcher matcher = pattern.matcher(url);
|
||||||
|
|
||||||
|
matcher.find();
|
||||||
|
|
||||||
|
String protocol = matcher.group(1);
|
||||||
|
String domain = matcher.group(2);
|
||||||
|
String port = matcher.group(3);
|
||||||
|
String uri = matcher.group(4);
|
||||||
|
```
|
110
dev/snippet/Сериализация и Десериализация даты в Jackson.md
Normal file
110
dev/snippet/Сериализация и Десериализация даты в Jackson.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
aliases:
|
||||||
|
tags:
|
||||||
|
- maturity/🌱
|
||||||
|
date:
|
||||||
|
- - 2023-11-20
|
||||||
|
zero-link:
|
||||||
|
- "[[../../../../garden/ru/meta/zero/00 Снипеты для Java|00 Снипеты для Java]]"
|
||||||
|
parents:
|
||||||
|
linked:
|
||||||
|
article: https://note.struchkov.dev/localdatetime-deserializer/
|
||||||
|
---
|
||||||
|
Чаще всего по работе я сталкиваюсь с проблемой десериализации и сериализации даты. Многие разработчики отступают от стандартного формата времени `yyyy-MM-dd'T'HH:mm:ss*SSSZZZZ` и изобретают свои форматы.
|
||||||
|
|
||||||
|
К сожалению, в Jackson не заложены все возможные форматы даты, поэтому необходимо написать свой десериализатор.
|
||||||
|
## Десериализация
|
||||||
|
В данном случае это преобразование json формата в Java объект. Необходимо расширить абстрактный класс `StdDeserializer`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
public class CustomDeserializer extends StdDeserializer<LocalDateTime> {
|
||||||
|
|
||||||
|
protected CustomDeserializer() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CustomDeserializer(Class<?> vc) {
|
||||||
|
super(vc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
|
||||||
|
String value = jsonParser.getText();
|
||||||
|
if (!"".equals(value)) {
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
|
||||||
|
return LocalDateTime.parse(value, formatter);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого необходимо над полем поставить аннотацию `@JsonDeserialize` c указанием нашего кастомного десериализатора.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Foo {
|
||||||
|
|
||||||
|
// ... ... ... ... ...
|
||||||
|
|
||||||
|
@JsonDeserialize(using = CustomDeserializer.class)
|
||||||
|
private LocalDateTime date;
|
||||||
|
|
||||||
|
// ... ... ... ... ...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Сериализация
|
||||||
|
В данном случае это преобразование Java объекта в json формат. Для сериализации необходимо расширить класс `StdSerializer`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
public class CustomSerializer extends StdSerializer<LocalDateTime> {
|
||||||
|
|
||||||
|
protected CustomSerializer(Class<LocalDateTime> t) {
|
||||||
|
super(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CustomSerializer() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss");
|
||||||
|
gen.writeString(formatter.format(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Для работы над полем поставить аннотацию `@JsonSerialize`
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Foo {
|
||||||
|
|
||||||
|
// ... ... ... ... ...
|
||||||
|
|
||||||
|
@JsonSerialize(using = LocalDateTimestampSerializer.class)
|
||||||
|
private LocalDateTime date;
|
||||||
|
|
||||||
|
// ... ... ... ... ...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
50
dev/snippet/Создание linux сервиса для Java приложения.md
Normal file
50
dev/snippet/Создание linux сервиса для Java приложения.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
aliases:
|
||||||
|
tags:
|
||||||
|
- maturity/🌱
|
||||||
|
date:
|
||||||
|
- - 2023-11-20
|
||||||
|
zero-link:
|
||||||
|
- "[[../../../../garden/ru/meta/zero/00 Снипеты для Java|00 Снипеты для Java]]"
|
||||||
|
parents:
|
||||||
|
linked: []
|
||||||
|
article: https://note.struchkov.dev/sozdaniie-linux-siervisa-dlia-prilozhieniia-spring-boot/
|
||||||
|
---
|
||||||
|
Для запуска `jar` файла в linux в виде сервиса, необходимо создать файл конфигурации.
|
||||||
|
|
||||||
|
```java
|
||||||
|
sudo nano /etc/systemd/system/app_name_service.service
|
||||||
|
```
|
||||||
|
|
||||||
|
В этот файл вставляем примерно следующее. Не забудьте заменить в выделенных строках `app_path`, `app_name`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
[Unit]
|
||||||
|
Description=App Description
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
SyslogIdentifier=appdescription
|
||||||
|
WorkingDirectory=/app_path
|
||||||
|
PIDFile=/app_path/app_name.pid
|
||||||
|
ExecStart=/bin/sh -c "exec /usr/bin/java -jar app_name.jar & echo $! > /app_path/app_name.pid"
|
||||||
|
ExecReload=/bin/kill -s HUP $MAINPID ExecStop=/bin/kill -s QUIT $MAINPID
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=forking
|
||||||
|
PIDFile=/app_path/app_name.pid
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Для автоматического запуска приложение после перезагрузки сервиса, используйте следующую команду.
|
||||||
|
```bash
|
||||||
|
systemctl enable app_service
|
||||||
|
```
|
||||||
|
|
||||||
|
Чтобы убрать приложения из автозагрузки:
|
||||||
|
```bash
|
||||||
|
systemctl disable app_service
|
||||||
|
```
|
32
dev/snippet/Удаление .DS_Store из Git репозитория.md
Normal file
32
dev/snippet/Удаление .DS_Store из Git репозитория.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- maturity/🌳
|
||||||
|
date: "[[2023-08-31]]"
|
||||||
|
parents:
|
||||||
|
- "[[../../meta/zero/00 Снипеты для Git|00 Снипеты для Git]]"
|
||||||
|
zero-link:
|
||||||
|
- "[[../../meta/zero/00 Разработка|00 Разработка]]"
|
||||||
|
article: https://struchkov.dev/blog/ru/remove-dsstore-git/
|
||||||
|
---
|
||||||
|
Симптомы: Есть не зафиксированный файл, который мешает гиту. Файл называется `.DS_Store`.
|
||||||
|
|
||||||
|
Чтобы удалить этот файл нужно выполнить следующие команды:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
find . -name .DS_Store -print0 | xargs -0 git rm --ignore-unmatch
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git rm --cached .DS_Store
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git add .
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git commit -m "Remove .DS_Store from current directory"
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!WARNING] Терминал в Idea
|
||||||
|
> Команды необходимо выполнять из обычного родного терминала. Если выполнять из терминала Idea, то они не сработают.
|
@ -1,8 +1,27 @@
|
|||||||
---
|
---
|
||||||
tags:
|
tags:
|
||||||
- type/zero-link
|
- type/zero-link
|
||||||
|
- type/moc
|
||||||
parents:
|
parents:
|
||||||
- "[[00 Разработка]]"
|
- "[[00 Разработка]]"
|
||||||
title: Java разработка
|
title: Java разработка
|
||||||
|
aliases: []
|
||||||
|
zero-link:
|
||||||
|
- "[[../../../garden/ru/meta/zero/00 Разработка]]"
|
||||||
---
|
---
|
||||||
- [Garbage Collector](../../dev/java/gc/Garbage%20Collector.md)
|
- [Устройство Java](Устройство%20Java.md)
|
||||||
|
- [Снипеты для Java](../../../garden/ru/meta/zero/00%20Снипеты%20для%20Java.md)
|
||||||
|
- Фреймворки
|
||||||
|
- [SpringBoot](00%20SpringBoot.md)
|
||||||
|
- [Quarkus](00%20Quarkus.md)
|
||||||
|
## Версии Java
|
||||||
|
- [[Java 1]]
|
||||||
|
- [Java 7](Java%207.md)
|
||||||
|
- [[Java 8 LTS]]
|
||||||
|
- [Java 9](Java%209.md)
|
||||||
|
- [[Java 10]]
|
||||||
|
- [Java 11 LTS](Java%2011%20LTS.md)
|
||||||
|
- [Java 12](Java%2012.md)
|
||||||
|
- [[Java 15]]
|
||||||
|
- [[Java 17 LTS]]
|
||||||
|
- [[Java 21 LTS]]
|
15
meta/zero/00 Quarkus.md
Normal file
15
meta/zero/00 Quarkus.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
aliases:
|
||||||
|
- кваркус
|
||||||
|
- кваркусе
|
||||||
|
- кваркусом
|
||||||
|
- 00 Quarkus
|
||||||
|
tags:
|
||||||
|
- type/zero-link
|
||||||
|
date:
|
||||||
|
- - 2024-03-03
|
||||||
|
zero-link:
|
||||||
|
- "[[00 Java разработка]]"
|
||||||
|
parents:
|
||||||
|
linked:
|
||||||
|
---
|
13
meta/zero/00 Snippets.md
Normal file
13
meta/zero/00 Snippets.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- type/zero-link
|
||||||
|
aliases:
|
||||||
|
date:
|
||||||
|
- - 2024-09-04
|
||||||
|
zero-link:
|
||||||
|
- "[[00 Разработка|00 Разработка]]"
|
||||||
|
parents:
|
||||||
|
linked:
|
||||||
|
---
|
||||||
|
- [00 Снипеты для Java](00%20Снипеты%20для%20Java.md)
|
||||||
|
- [00 Снипеты для Git](00%20Снипеты%20для%20Git.md)
|
10
meta/zero/00 SpringBoot.md
Normal file
10
meta/zero/00 SpringBoot.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
aliases:
|
||||||
|
tags:
|
||||||
|
- type/zero-link
|
||||||
|
date: 2023-11-15
|
||||||
|
zero-link:
|
||||||
|
- "[[../../../../garden/ru/meta/zero/00 Java разработка|00 Java разработка]]"
|
||||||
|
parents: []
|
||||||
|
linked:
|
||||||
|
---
|
7
meta/zero/00 Снипеты для Git.md
Normal file
7
meta/zero/00 Снипеты для Git.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- type/zero-link
|
||||||
|
zero-link:
|
||||||
|
- "[[00 Snippets|00 Snippets]]"
|
||||||
|
---
|
||||||
|
- [Удаление .DS_Store из Git репозитория](../../dev/snippet/Удаление%20.DS_Store%20из%20Git%20репозитория.md)
|
13
meta/zero/00 Снипеты для Java.md
Normal file
13
meta/zero/00 Снипеты для Java.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- type/zero-link
|
||||||
|
zero-link:
|
||||||
|
- "[[00 Snippets|00 Snippets]]"
|
||||||
|
aliases:
|
||||||
|
- Скрипты на Java
|
||||||
|
---
|
||||||
|
- [Парсинг URL c помощью регулярки](../../dev/snippet/Парсинг%20URL%20c%20помощью%20регулярки.md)
|
||||||
|
- [Сериализация и Десериализация даты в Jackson](../../dev/snippet/Сериализация%20и%20Десериализация%20даты%20в%20Jackson.md)
|
||||||
|
- [Создание linux сервиса для Java приложения](../../dev/snippet/Создание%20linux%20сервиса%20для%20Java%20приложения.md)
|
||||||
|
- [Игнорирование ошибок сертификата OkHttp3](../../dev/snippet/Игнорирование%20ошибок%20сертификата%20OkHttp3.md)
|
||||||
|
- [Mock конфигурация Oauth2 для SpringBoot](../../dev/snippet/Mock%20конфигурация%20Oauth2%20для%20SpringBoot.md)
|
Loading…
Reference in New Issue
Block a user