Compare commits
4 Commits
release/8.
...
release/8.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ab77c2d84 | ||
|
|
ff2f9fd884 | ||
|
|
b5df336d2f | ||
|
|
ef2db192d1 |
@@ -0,0 +1,233 @@
|
||||
// ============================================================================
|
||||
//
|
||||
// Copyright (C) 2006-2021 Talend Inc. - www.talend.com
|
||||
//
|
||||
// This source code is available under agreement available at
|
||||
// %InstallDIR%\features\org.talend.rcp.branding.%PRODUCTNAME%\%PRODUCTNAME%license.txt
|
||||
//
|
||||
// You should have received a copy of the agreement
|
||||
// along with this program; if not, write to Talend SA
|
||||
// 9 rue Pages 92150 Suresnes, France
|
||||
//
|
||||
// ============================================================================
|
||||
package org.talend.commons.ui.swt.formtools;
|
||||
|
||||
import org.eclipse.swt.SWT;
|
||||
import org.eclipse.swt.events.FocusListener;
|
||||
import org.eclipse.swt.events.SelectionListener;
|
||||
import org.eclipse.swt.layout.GridData;
|
||||
import org.eclipse.swt.widgets.Button;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.Control;
|
||||
import org.eclipse.swt.widgets.Label;
|
||||
import org.eclipse.swt.widgets.Listener;
|
||||
|
||||
/**
|
||||
* Create a Label and a Checkbox.
|
||||
*/
|
||||
public class LabelledCheckbox implements LabelledWidget{
|
||||
|
||||
private Button button;
|
||||
|
||||
private Label label;
|
||||
|
||||
/**
|
||||
* Create a Label and a Text.
|
||||
*
|
||||
* @param composite
|
||||
* @param string
|
||||
*/
|
||||
public LabelledCheckbox(Composite composite, String string) {
|
||||
createLabelledButton(composite, string, 1, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Label and a Text.
|
||||
*
|
||||
* @param composite
|
||||
* @param string
|
||||
* @param isFill
|
||||
*/
|
||||
public LabelledCheckbox(Composite composite, String string, boolean isFill) {
|
||||
createLabelledButton(composite, string, 1, isFill);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Label and a Button width specific styleField.
|
||||
*
|
||||
* @param composite
|
||||
* @param string
|
||||
* @param int horizontalSpan
|
||||
*/
|
||||
public LabelledCheckbox(Composite composite, String string, int horizontalSpan) {
|
||||
createLabelledButton(composite, string, horizontalSpan, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Label and a Button width specific styleField.
|
||||
*
|
||||
* @param composite
|
||||
* @param string
|
||||
* @param int horizontalSpan
|
||||
* @param styleField
|
||||
*/
|
||||
public LabelledCheckbox(Composite composite, String string, int horizontalSpan, int styleField) {
|
||||
createLabelledButton(composite, string, horizontalSpan, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Label and a Button width Gridata option FILL.
|
||||
*
|
||||
* @param composite
|
||||
* @param string
|
||||
* @param styleField
|
||||
* @param int horizontalSpan
|
||||
* @param isFill
|
||||
*/
|
||||
public LabelledCheckbox(Composite composite, String string, int horizontalSpan, boolean isFill) {
|
||||
createLabelledButton(composite, string, horizontalSpan, isFill);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Label and a Button width specific styleField and Gridata option FILL.
|
||||
*
|
||||
* @param composite
|
||||
* @param string
|
||||
* @param int horizontalSpan
|
||||
* @param styleField
|
||||
* @param isFill
|
||||
*/
|
||||
public LabelledCheckbox(Composite composite, String string, int horizontalSpan, int styleField, boolean isFill) {
|
||||
createLabelledButton(composite, string, horizontalSpan, isFill);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Label and a Button width specific styleField and Gridata option FILL.
|
||||
*
|
||||
* @param composite
|
||||
* @param string
|
||||
* @param int horizontalSpan
|
||||
* @param styleField
|
||||
* @param isFill
|
||||
*/
|
||||
private void createLabelledButton(Composite composite, String string, int horizontalSpan, boolean isFill) {
|
||||
label = new Label(composite, SWT.LEFT);
|
||||
if (string != null) {
|
||||
label.setText(string);
|
||||
}
|
||||
|
||||
button = new Button(composite, SWT.CHECK);
|
||||
int gridDataStyle = SWT.NONE;
|
||||
if (isFill) {
|
||||
gridDataStyle = SWT.FILL;
|
||||
}
|
||||
GridData gridData = new GridData(gridDataStyle, SWT.CENTER, true, false);
|
||||
gridData.horizontalSpan = horizontalSpan;
|
||||
button.setLayoutData(gridData);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* setToolTipText to Text Object.
|
||||
*
|
||||
* @param string
|
||||
*/
|
||||
public void setToolTipText(final String string) {
|
||||
button.setToolTipText(string);
|
||||
}
|
||||
|
||||
/**
|
||||
* is Checkbox Selected.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public Boolean isSelected() {
|
||||
return button.getSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* setText to Label Object.
|
||||
*
|
||||
* @param string
|
||||
*/
|
||||
public void setLabelText(final String string) {
|
||||
if (string != null) {
|
||||
label.setText(string);
|
||||
} else {
|
||||
label.setText(""); //$NON-NLS-1$
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* setEditable to Button and Label Object.
|
||||
*
|
||||
* @param boolean
|
||||
*/
|
||||
public void forceFocus() {
|
||||
setEnabled(true);
|
||||
button.forceFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* setEnabled to Button and Label Object.
|
||||
*
|
||||
* @param boolean
|
||||
*/
|
||||
public void setEnabled(final boolean visible) {
|
||||
button.setEnabled(visible);
|
||||
label.setEnabled(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* setVisible to Button and Label Object.
|
||||
*
|
||||
* @param boolean
|
||||
*/
|
||||
public void setVisible(final boolean visible) {
|
||||
button.setVisible(visible);
|
||||
label.setVisible(visible);
|
||||
}
|
||||
|
||||
public void setVisible(final boolean visible, final boolean exclude) {
|
||||
Control[] controls = new Control[] { label, button };
|
||||
for (Control control : controls) {
|
||||
control.setVisible(visible);
|
||||
if (control.getLayoutData() instanceof GridData) {
|
||||
((GridData) control.getLayoutData()).exclude = exclude;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* addListener to Button Object.
|
||||
*
|
||||
* @param eventType
|
||||
* @param listener
|
||||
*/
|
||||
public void addListener(int eventType, Listener listener) {
|
||||
button.addListener(eventType, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* addFocusListener to Button Object.
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public void addFocusListener(FocusListener listener) {
|
||||
button.addFocusListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(String value) {
|
||||
button.setSelection(Boolean.parseBoolean(value));
|
||||
}
|
||||
|
||||
public void addSelectionListener(SelectionListener listener) {
|
||||
button.addSelectionListener(listener);
|
||||
}
|
||||
|
||||
public boolean getSelection() {
|
||||
return button.getSelection();
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ import org.eclipse.swt.widgets.Text;
|
||||
* $Id: LabelledText.java 7038 2007-11-15 14:05:48Z plegall $
|
||||
*
|
||||
*/
|
||||
public class LabelledText {
|
||||
public class LabelledText implements LabelledWidget{
|
||||
|
||||
private Text text;
|
||||
|
||||
@@ -297,6 +297,16 @@ public class LabelledText {
|
||||
label.setVisible(visible);
|
||||
}
|
||||
|
||||
public void setVisible(final boolean visible, final boolean exclude) {
|
||||
Control[] controls = new Control[] { label, text };
|
||||
for (Control control : controls) {
|
||||
control.setVisible(visible);
|
||||
if (control.getLayoutData() instanceof GridData) {
|
||||
((GridData) control.getLayoutData()).exclude = exclude;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isVisiable() {
|
||||
return text.isVisible() && label.isVisible();
|
||||
}
|
||||
@@ -472,4 +482,9 @@ public class LabelledText {
|
||||
text.getParent().layout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(String value) {
|
||||
this.setText(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.talend.commons.ui.swt.formtools;
|
||||
|
||||
public interface LabelledWidget {
|
||||
|
||||
// Set the value of the widget
|
||||
public void set(String value);
|
||||
|
||||
public void setVisible(boolean visible, boolean exclude);
|
||||
}
|
||||
@@ -324,17 +324,23 @@ public class ConnParameterKeys {
|
||||
/**
|
||||
* Google Dataproc keys.
|
||||
*/
|
||||
public static final String CONN_PARA_KEY_GOOGLE_PROJECT_ID = "CONN_PARA_KEY_GOOGLE_PROJECT_ID"; //$NON-NLS-1$
|
||||
public static final String CONN_PARA_KEY_GOOGLE_PROJECT_ID = "CONN_PARA_KEY_GOOGLE_PROJECT_ID";
|
||||
|
||||
public static final String CONN_PARA_KEY_GOOGLE_CLUSTER_ID = "CONN_PARA_KEY_GOOGLE_CLUSTER_ID";//$NON-NLS-1$
|
||||
|
||||
public static final String CONN_PARA_KEY_GOOGLE_REGION = "CONN_PARA_KEY_GOOGLE_REGION"; //$NON-NLS-1$
|
||||
|
||||
public static final String CONN_PARA_KEY_GOOGLE_JARS_BUCKET = "CONN_PARA_KEY_GOOGLE_JARS_BUCKET"; //$NON-NLS-1$
|
||||
public static final String CONN_PARA_KEY_GOOGLE_CLUSTER_ID = "CONN_PARA_KEY_GOOGLE_CLUSTER_ID";
|
||||
|
||||
public static final String CONN_PARA_KEY_GOOGLE_REGION = "CONN_PARA_KEY_GOOGLE_REGION";
|
||||
|
||||
public static final String CONN_PARA_KEY_GOOGLE_JARS_BUCKET = "CONN_PARA_KEY_GOOGLE_JARS_BUCKET";
|
||||
|
||||
public static final String CONN_PARA_KEY_PROVIDE_GOOGLE_CREDENTIALS = "CONN_PARA_KEY_PROVIDE_GOOGLE_CREDENTIALS";
|
||||
|
||||
public static final String CONN_PARA_KEY_AUTH_MODE = "CONN_PARA_KEY_AUTH_MODE";
|
||||
|
||||
public static final String CONN_PARA_KEY_DEFINE_PATH_TO_GOOGLE_CREDENTIALS = "CONN_PARA_KEY_DEFINE_PATH_TO_GOOGLE_CREDENTIALS"; //$NON-NLS-1$
|
||||
|
||||
public static final String CONN_PARA_KEY_PATH_TO_GOOGLE_CREDENTIALS = "CONN_PARA_KEY_PATH_TO_GOOGLE_CREDENTIALS"; //$NON-NLS-1$
|
||||
|
||||
public static final String CONN_PARA_OAUTH2_TOKEN_TO_GOOGLE_CREDENTIALS = "CONN_PARA_OAUTH2_TOKEN_TO_GOOGLE_CREDENTIALS"; //$NON-NLS-1$
|
||||
|
||||
/**DataBricks*/
|
||||
public static final String CONN_PARA_KEY_DATABRICKS_ENDPOINT="CONN_PARA_KEY_DATABRICKS_ENDPOINT";
|
||||
@@ -369,6 +375,14 @@ public class ConnParameterKeys {
|
||||
|
||||
public static final String CONN_PARA_KEY_KNOX_DIRECTORY="CONN_PARA_KEY_KNOX_DIRECTORY";
|
||||
|
||||
// CDE
|
||||
public static final String CONN_PARA_KEY_CDE_API_ENDPOINT="CONN_PARA_KEY_CDE_API_ENDPOINT";
|
||||
public static final String CONN_PARA_KEY_CDE_TOKEN="CONN_PARA_KEY_CDE_TOKEN";
|
||||
public static final String CONN_PARA_KEY_CDE_AUTO_GENERATE_TOKEN="CONN_PARA_KEY_CDE_AUTO_GENERATE_TOKEN";
|
||||
public static final String CONN_PARA_KEY_CDE_TOKEN_ENDPOINT="CONN_PARA_KEY_CDE_TOKEN_ENDPOINT";
|
||||
public static final String CONN_PARA_KEY_CDE_WORKLOAD_USER="CONN_PARA_KEY_CDE_WORKLOAD_USER";
|
||||
public static final String CONN_PARA_KEY_CDE_WORKLOAD_PASSWORD = "CONN_PARA_KEY_CDE_WORKLOAD_PASSWORD";
|
||||
|
||||
/**
|
||||
* Redshift
|
||||
*/
|
||||
|
||||
@@ -83,6 +83,12 @@ public enum EHadoopProperties {
|
||||
GOOGLE_REGION,
|
||||
|
||||
GOOGLE_JARS_BUCKET,
|
||||
|
||||
AUTH_MODE,
|
||||
|
||||
PATH_TO_GOOGLE_CREDENTIALS,
|
||||
|
||||
OAUTH_ACCESS_TOKEN,
|
||||
|
||||
HD_WEBHCAT_HOSTNAME,
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// ============================================================================
|
||||
//
|
||||
// Copyright (C) 2006-2020 Talend Inc. - www.talend.com
|
||||
//
|
||||
// This source code is available under agreement available at
|
||||
// %InstallDIR%\features\org.talend.rcp.branding.%PRODUCTNAME%\%PRODUCTNAME%license.txt
|
||||
//
|
||||
// You should have received a copy of the agreement
|
||||
// along with this program; if not, write to Talend SA
|
||||
// 9 rue Pages 92150 Suresnes, France
|
||||
//
|
||||
// ============================================================================
|
||||
package org.talend.core.hadoop.version;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public enum EDataprocAuthType {
|
||||
|
||||
OAUTH_API("OAuth2 Access Token"), //$NON-NLS-1$
|
||||
|
||||
SERVICE_ACCOUNT("Service account"); //$NON-NLS-1$
|
||||
|
||||
private String displayName;
|
||||
|
||||
EDataprocAuthType(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name();
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
public static List<String> getAllDataprocAuthTypes() {
|
||||
return getAllDataprocAuthTypes(true);
|
||||
}
|
||||
|
||||
public static List<String> getAllDataprocAuthTypes(boolean display) {
|
||||
List<String> types = new ArrayList<String>();
|
||||
EDataprocAuthType[] values = values();
|
||||
for (EDataprocAuthType authType : values) {
|
||||
if (display) {
|
||||
types.add(authType.getDisplayName());
|
||||
} else {
|
||||
types.add(authType.getName());
|
||||
}
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
public static EDataprocAuthType getDataprocAuthTypeByDisplayName(String name) {
|
||||
return getDataprocAuthTypeByName(name, true);
|
||||
}
|
||||
|
||||
public static EDataprocAuthType getDataprocAuthTypeByName(String type, boolean display) {
|
||||
if (type != null) {
|
||||
for (EDataprocAuthType authType : values()) {
|
||||
if (display) {
|
||||
if (type.equalsIgnoreCase(authType.getDisplayName())) {
|
||||
return authType;
|
||||
}
|
||||
} else {
|
||||
if (type.equalsIgnoreCase(authType.getName())) {
|
||||
return authType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,17 @@
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
<url>${talend.nexus.host}/nexus/content/repositories/snapshots/</url>
|
||||
<url>${talend.nexus.host}/nexus/content/repositories/TalendOpenSourceSnapshot/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>releases</id>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
<url>${talend.nexus.host}/nexus/content/repositories/TalendOpenSourceRelease/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
@@ -27,6 +37,7 @@
|
||||
<studioIndexVersion>${project.version}</studioIndexVersion>
|
||||
<talend.nexus.host>https://artifacts-oss.talend.com</talend.nexus.host>
|
||||
<talend.nexus.host.oss>https://artifacts-oss.talend.com</talend.nexus.host.oss>
|
||||
<tycho.buildtimestamp.format>${timestamp}</tycho.buildtimestamp.format>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -144,7 +144,10 @@ public class ExtendedNodeConnectionContextUtils {
|
||||
GoogleClusterId,
|
||||
GoogleRegion,
|
||||
GoogleJarsBucket,
|
||||
useGoogleCredentials,
|
||||
GoogleAuthMode,
|
||||
PathToGoogleCredentials,
|
||||
GoogleOauthToken,
|
||||
|
||||
// Override hadoop configuration
|
||||
setHadoopConf,
|
||||
@@ -164,7 +167,15 @@ public class ExtendedNodeConnectionContextUtils {
|
||||
KnoxUrl,
|
||||
KnoxUsername,
|
||||
KnoxPassword,
|
||||
KnoxDirectory
|
||||
KnoxDirectory,
|
||||
|
||||
//Cde
|
||||
CdeApiEndPoint,
|
||||
CdeAutoGenerateToken,
|
||||
CdeToken,
|
||||
CdeTokenEndpoint,
|
||||
CdeWorkloadUser,
|
||||
CdeWorkloadPassword
|
||||
}
|
||||
|
||||
static List<IContextParameter> getContextVariables(final String prefixName, Connection conn, Set<IConnParamName> paramSet) {
|
||||
|
||||
@@ -483,7 +483,7 @@ public abstract class AbstractForm extends Composite {
|
||||
} else {
|
||||
String defaultContextName = ConnectionContextHelper.convertContextLabel(connectionItem.getProperty().getLabel());
|
||||
Map<ContextItem, List<ConectionAdaptContextVariableModel>> variableModels = ConnectionContextHelper
|
||||
.exportAsContext(defaultContextName, connectionItem, getConetxtParams());
|
||||
.exportAsContext(defaultContextName, connectionItem, getContextParams());
|
||||
contextManager = ConnectionContextHelper.contextManager;
|
||||
|
||||
if (variableModels != null) {
|
||||
@@ -496,11 +496,11 @@ public abstract class AbstractForm extends Composite {
|
||||
Map<String, String> map = ((JobContextManager) contextManager).getNameMap();
|
||||
// set properties for context mode
|
||||
ConnectionContextHelper.setPropertiesForContextMode(defaultContextName, connectionItem,
|
||||
contextItem, getConetxtParams(), map);
|
||||
contextItem, getContextParams(), map);
|
||||
}
|
||||
} else {
|
||||
// set properties for exist context
|
||||
ConnectionContextHelper.setPropertiesForExistContextMode(connectionItem, getConetxtParams(),
|
||||
ConnectionContextHelper.setPropertiesForExistContextMode(connectionItem, getContextParams(),
|
||||
variableModels);
|
||||
}
|
||||
// refresh current UI.
|
||||
@@ -556,7 +556,7 @@ public abstract class AbstractForm extends Composite {
|
||||
contextParamSet.clear();
|
||||
}
|
||||
|
||||
protected Set<IConnParamName> getConetxtParams() {
|
||||
protected Set<IConnParamName> getContextParams() {
|
||||
return contextParamSet; // for db and file connection
|
||||
}
|
||||
|
||||
|
||||
@@ -7217,7 +7217,7 @@ public class DatabaseForm extends AbstractForm {
|
||||
private void collectHiveContextParams() {
|
||||
// recollect context params for hive
|
||||
if (isHiveDBConnSelected()) {
|
||||
getConetxtParams().clear();
|
||||
getContextParams().clear();
|
||||
addContextParams(EDBParamName.Database, true);
|
||||
boolean isHiveDataproc = doSupportHiveDataproc();
|
||||
if (isHiveDataproc) {
|
||||
@@ -7262,7 +7262,7 @@ public class DatabaseForm extends AbstractForm {
|
||||
private void collectHBaseContextParams() {
|
||||
// recollect context params for Hbase
|
||||
if (isHBaseDBConnSelected()) {
|
||||
getConetxtParams().clear();
|
||||
getContextParams().clear();
|
||||
addContextParams(EDBParamName.Server, true);
|
||||
addContextParams(EDBParamName.Port, true);
|
||||
addContextParams(EDBParamName.Schema, true);
|
||||
@@ -7284,7 +7284,7 @@ public class DatabaseForm extends AbstractForm {
|
||||
private void collectMaprdbContextParams() {
|
||||
// recollect context params for maprdb
|
||||
if (isMapRDBConnSelected()) {
|
||||
getConetxtParams().clear();
|
||||
getContextParams().clear();
|
||||
addContextParams(EDBParamName.Server, true);
|
||||
addContextParams(EDBParamName.Port, true);
|
||||
addContextParams(EDBParamName.Schema, true);
|
||||
@@ -7306,7 +7306,7 @@ public class DatabaseForm extends AbstractForm {
|
||||
private void collectImpalaContextParams() {
|
||||
// recollect context params for impala
|
||||
if (isImpalaDBConnSelected()) {
|
||||
getConetxtParams().clear();
|
||||
getContextParams().clear();
|
||||
addContextParams(EDBParamName.Login, true);
|
||||
addContextParams(EDBParamName.Server, true);
|
||||
addContextParams(EDBParamName.Port, true);
|
||||
@@ -7328,7 +7328,7 @@ public class DatabaseForm extends AbstractForm {
|
||||
private void collectOracleCustomContextParams() {
|
||||
// recollect context params for Oracle Custom
|
||||
if (isOracleCustomDBConnSelected()) {
|
||||
getConetxtParams().clear();
|
||||
getContextParams().clear();
|
||||
addContextParams(EDBParamName.Server, true);
|
||||
addContextParams(EDBParamName.Password, true);
|
||||
addContextParams(EDBParamName.Login, true);
|
||||
@@ -9516,7 +9516,7 @@ public class DatabaseForm extends AbstractForm {
|
||||
labelText = sidOrDatabaseText.getLabelText();
|
||||
needConfirmDialog = true;
|
||||
}
|
||||
Set<IConnParamName> conetxtParams = getConetxtParams();
|
||||
Set<IConnParamName> conetxtParams = getContextParams();
|
||||
boolean contains = conetxtParams.contains(EDBParamName.Schema);
|
||||
if ((contains || EDatabaseConnTemplate.isSchemaNeeded(getConnection().getDatabaseType()))
|
||||
&& StringUtils.isBlank(schemaText.getText())) {
|
||||
|
||||
Reference in New Issue
Block a user