From f76fee63ed9cb3a30d3c0c092d860b1cb93a481b Mon Sep 17 00:00:00 2001
From: Gerard Smyth <gerard.smyth@gmail.com>
Date: Thu, 08 May 2014 13:09:30 -0400
Subject: [PATCH] Updated the SyndicationServlet to provide an additional option to return details of the tags in the repository instead of the commits. This uses a new 'ot' request parameter to indicate the object type of the content to return, which can be ither TAG or COMMIT. If this is not provided, then COMMIT is assumed to maintain backwards compatability. If tags are returned, then the paging parameters, 'l' and 'pg' are still supported, but searching options are currently ignored.

---
 src/main/java/com/gitblit/authority/GitblitAuthority.java |  219 ++++++++++++++++++++++++++----------------------------
 1 files changed, 105 insertions(+), 114 deletions(-)

diff --git a/src/main/java/com/gitblit/authority/GitblitAuthority.java b/src/main/java/com/gitblit/authority/GitblitAuthority.java
index 1a1f96d..5f4a7e7 100644
--- a/src/main/java/com/gitblit/authority/GitblitAuthority.java
+++ b/src/main/java/com/gitblit/authority/GitblitAuthority.java
@@ -50,12 +50,7 @@
 import java.util.List;
 import java.util.Map;
 
-import javax.activation.DataHandler;
-import javax.activation.FileDataSource;
 import javax.mail.Message;
-import javax.mail.Multipart;
-import javax.mail.internet.MimeBodyPart;
-import javax.mail.internet.MimeMultipart;
 import javax.swing.ImageIcon;
 import javax.swing.InputVerifier;
 import javax.swing.JButton;
@@ -90,10 +85,11 @@
 import com.gitblit.IStoredSettings;
 import com.gitblit.IUserService;
 import com.gitblit.Keys;
-import com.gitblit.MailExecutor;
 import com.gitblit.client.HeaderPanel;
 import com.gitblit.client.Translation;
+import com.gitblit.models.Mailing;
 import com.gitblit.models.UserModel;
+import com.gitblit.service.MailService;
 import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.FileUtils;
 import com.gitblit.utils.StringUtils;
@@ -105,33 +101,33 @@
 
 /**
  * Simple GUI tool for administering Gitblit client certificates.
- * 
+ *
  * @author James Moger
  *
  */
 public class GitblitAuthority extends JFrame implements X509Log {
 
 	private static final long serialVersionUID = 1L;
-	
+
 	private final UserCertificateTableModel tableModel;
 
 	private UserCertificatePanel userCertificatePanel;
-	
+
 	private File folder;
-	
+
 	private IStoredSettings gitblitSettings;
-	
+
 	private IUserService userService;
-	
+
 	private String caKeystorePassword;
 
 	private JTable table;
-	
+
 	private int defaultDuration;
-	
+
 	private TableRowSorter<UserCertificateTableModel> defaultSorter;
-	
-	private MailExecutor mail;
+
+	private MailService mail;
 
 	private JButton certificateDefaultsButton;
 
@@ -146,7 +142,7 @@
 				if (i + 1 == args.length) {
 					System.out.println("Invalid --baseFolder parameter!");
 					System.exit(-1);
-				} else if (args[i + 1] != ".") {
+				} else if (!".".equals(args[i + 1])) {
 					folder = args[i+1];
 				}
 				break;
@@ -154,6 +150,7 @@
 		}
 		final String baseFolder = folder;
 		EventQueue.invokeLater(new Runnable() {
+			@Override
 			public void run() {
 				try {
 					UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
@@ -172,7 +169,7 @@
 		tableModel = new UserCertificateTableModel();
 		defaultSorter = new TableRowSorter<UserCertificateTableModel>(tableModel);
 	}
-	
+
 	public void initialize(String baseFolder) {
 		setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
 		setTitle("Gitblit Certificate Authority v" + Constants.getVersion() + " (" + Constants.getBuildDate() + ")");
@@ -187,14 +184,14 @@
 			@Override
 			public void windowOpened(WindowEvent event) {
 			}
-		});		
+		});
 
 		File folder = new File(baseFolder).getAbsoluteFile();
 		load(folder);
-		
+
 		setSizeAndPosition();
 	}
-	
+
 	private void setSizeAndPosition() {
 		String sz = null;
 		String pos = null;
@@ -243,30 +240,27 @@
 			Utils.showException(GitblitAuthority.this, t);
 		}
 	}
-	
+
 	private StoredConfig getConfig() throws IOException, ConfigInvalidException {
 		File configFile  = new File(folder, X509Utils.CA_CONFIG);
 		FileBasedConfig config = new FileBasedConfig(configFile, FS.detect());
 		config.load();
 		return config;
 	}
-	
+
 	private IUserService loadUsers(File folder) {
 		File file = new File(folder, "gitblit.properties");
 		if (!file.exists()) {
 			return null;
 		}
 		gitblitSettings = new FileSettings(file.getAbsolutePath());
-		mail = new MailExecutor(gitblitSettings);
+		mail = new MailService(gitblitSettings);
 		String us = gitblitSettings.getString(Keys.realm.userService, "${baseFolder}/users.conf");
 		String ext = us.substring(us.lastIndexOf(".") + 1).toLowerCase();
 		IUserService service = null;
-		if (!ext.equals("conf") && !ext.equals("properties")) {
-			if (us.equals("com.gitblit.LdapUserService")) {
-				us = gitblitSettings.getString(Keys.realm.ldap.backingUserService, "${baseFolder}/users.conf");		
-			} else if (us.equals("com.gitblit.LdapUserService")) {
-				us = gitblitSettings.getString(Keys.realm.redmine.backingUserService, "${baseFolder}/users.conf");
-			}
+		if (!ext.equals("conf") && !ext.equals("properties") && ext.contains("userservice")) {
+			String realm = ext.substring(0, ext.indexOf("userservice"));
+			us = gitblitSettings.getString(MessageFormat.format("realm.{0}.backingUserService", realm), "${baseFolder}/users.conf");
 		}
 
 		if (us.endsWith(".conf")) {
@@ -274,11 +268,11 @@
 		} else {
 			throw new RuntimeException("Unsupported user service: " + us);
 		}
-		
+
 		service = new ConfigUserService(FileUtils.resolveParameter(Constants.baseFolder$, folder, us));
 		return service;
 	}
-	
+
 	private void load(File folder) {
 		this.folder = folder;
 		this.userService = loadUsers(folder);
@@ -290,7 +284,7 @@
 			Map<String, UserCertificateModel> map = new HashMap<String, UserCertificateModel>();
 			for (String user : userService.getAllUsernames()) {
 				UserModel model = userService.getUserModel(user);
-				UserCertificateModel ucm = new UserCertificateModel(model);				
+				UserCertificateModel ucm = new UserCertificateModel(model);
 				map.put(user, ucm);
 			}
 			File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
@@ -299,8 +293,8 @@
 				try {
 					config.load();
 					// replace user certificate model with actual data
-					List<UserCertificateModel> list = UserCertificateConfig.KEY.parse(config).list;					
-					for (UserCertificateModel ucm : list) {						
+					List<UserCertificateModel> list = UserCertificateConfig.KEY.parse(config).list;
+					for (UserCertificateModel ucm : list) {
 						ucm.user = userService.getUserModel(ucm.user.username);
 						map.put(ucm.user.username, ucm);
 					}
@@ -310,15 +304,15 @@
 					e.printStackTrace();
 				}
 			}
-			
+
 			tableModel.list = new ArrayList<UserCertificateModel>(map.values());
 			Collections.sort(tableModel.list);
 			tableModel.fireTableDataChanged();
 			Utils.packColumns(table, Utils.MARGIN);
-			
+
 			File caKeystore = new File(folder, X509Utils.CA_KEY_STORE);
 			if (!caKeystore.exists()) {
-				
+
 				if (!X509Utils.unlimitedStrength) {
 					// prompt to confirm user understands JCE Standard Strength encryption
 					int res = JOptionPane.showConfirmDialog(GitblitAuthority.this, Translation.get("gb.jceWarning"),
@@ -335,16 +329,16 @@
 						System.exit(1);
 					}
 				}
-				
-				// show certificate defaults dialog 
+
+				// show certificate defaults dialog
 				certificateDefaultsButton.doClick();
-				
+
 				// create "localhost" ssl certificate
 				prepareX509Infrastructure();
 			}
 		}
 	}
-	
+
 	private boolean prepareX509Infrastructure() {
 		if (caKeystorePassword == null) {
 			JPasswordField pass = new JPasswordField(10);
@@ -367,7 +361,7 @@
 		X509Utils.prepareX509Infrastructure(metadata, folder, this);
 		return true;
 	}
-	
+
 	private List<X509Certificate> findCerts(File folder, String username) {
 		List<X509Certificate> list = new ArrayList<X509Certificate>();
 		File userFolder = new File(folder, X509Utils.CERTS + File.separator + username);
@@ -382,7 +376,7 @@
 		});
 		try {
 			CertificateFactory factory = CertificateFactory.getInstance("X.509");
-			for (File cert : certs) {				
+			for (File cert : certs) {
 				BufferedInputStream is = new BufferedInputStream(new FileInputStream(cert));
 				X509Certificate x509 = (X509Certificate) factory.generateCertificate(is);
 				is.close();
@@ -393,16 +387,16 @@
 		}
 		return list;
 	}
-	
-	private Container getUI() {		
+
+	private Container getUI() {
 		userCertificatePanel = new UserCertificatePanel(this) {
-			
+
 			private static final long serialVersionUID = 1L;
 			@Override
 			public Insets getInsets() {
 				return Utils.INSETS;
 			}
-			
+
 			@Override
 			public boolean isAllowEmail() {
 				return mail.isReady();
@@ -418,12 +412,12 @@
 				c.set(Calendar.MILLISECOND, 0);
 				return c.getTime();
 			}
-			
+
 			@Override
 			public boolean saveUser(String username, UserCertificateModel ucm) {
 				return userService.updateUserModel(username, ucm.user);
 			}
-			
+
 			@Override
 			public boolean newCertificate(UserCertificateModel ucm, X509Metadata metadata, boolean sendEmail) {
 				if (!prepareX509Infrastructure()) {
@@ -433,9 +427,9 @@
 				Date notAfter = metadata.notAfter;
 				setMetadataDefaults(metadata);
 				metadata.notAfter = notAfter;
-				
+
 				// set user's specified OID values
-				UserModel user = ucm.user;				
+				UserModel user = ucm.user;
 				if (!StringUtils.isEmpty(user.organizationalUnit)) {
 					metadata.oids.put("OU", user.organizationalUnit);
 				}
@@ -459,21 +453,21 @@
 				if (ucm.expires == null || metadata.notAfter.before(ucm.expires)) {
 					ucm.expires = metadata.notAfter;
 				}
-				
+
 				updateAuthorityConfig(ucm);
-				
+
 				// refresh user
 				ucm.certs = null;
-				int modelIndex = table.convertRowIndexToModel(table.getSelectedRow());
+				int selectedIndex = table.getSelectedRow();
 				tableModel.fireTableDataChanged();
-				table.getSelectionModel().setSelectionInterval(modelIndex, modelIndex);
-				
+				table.getSelectionModel().setSelectionInterval(selectedIndex, selectedIndex);
+
 				if (sendEmail) {
 					sendEmail(user, metadata, zip);
 				}
 				return true;
 			}
-			
+
 			@Override
 			public boolean revoke(UserCertificateModel ucm, X509Certificate cert, RevocationReason reason) {
 				if (!prepareX509Infrastructure()) {
@@ -500,20 +494,20 @@
 					} catch (Exception e) {
 						Utils.showException(GitblitAuthority.this, e);
 					}
-					
+
 					// refresh user
 					ucm.certs = null;
 					int modelIndex = table.convertRowIndexToModel(table.getSelectedRow());
 					tableModel.fireTableDataChanged();
 					table.getSelectionModel().setSelectionInterval(modelIndex, modelIndex);
-					
+
 					return true;
 				}
-				
+
 				return false;
 			}
 		};
-		
+
 		table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
 		table.setRowSorter(defaultSorter);
 		table.setDefaultRenderer(CertificateStatus.class, new CertificateStatusRenderer());
@@ -536,9 +530,9 @@
 				userCertificatePanel.setUserCertificateModel(ucm);
 			}
 		});
-		
+
 		JPanel usersPanel = new JPanel(new BorderLayout()) {
-			
+
 			private static final long serialVersionUID = 1L;
 
 			@Override
@@ -549,10 +543,10 @@
 		usersPanel.add(new HeaderPanel(Translation.get("gb.users"), "users_16x16.png"), BorderLayout.NORTH);
 		usersPanel.add(new JScrollPane(table), BorderLayout.CENTER);
 		usersPanel.setMinimumSize(new Dimension(400, 10));
-		
+
 		certificateDefaultsButton = new JButton(new ImageIcon(getClass().getResource("/settings_16x16.png")));
 		certificateDefaultsButton.setFocusable(false);
-		certificateDefaultsButton.setToolTipText(Translation.get("gb.newCertificateDefaults"));		
+		certificateDefaultsButton.setToolTipText(Translation.get("gb.newCertificateDefaults"));
 		certificateDefaultsButton.addActionListener(new ActionListener() {
 			@Override
 			public void actionPerformed(ActionEvent e) {
@@ -570,6 +564,7 @@
 					certificateConfig.update(metadata);
 				}
 				InputVerifier verifier = new InputVerifier() {
+					@Override
 					public boolean verify(JComponent comp) {
 						boolean returnValue;
 						JTextField textField = (JTextField) comp;
@@ -594,18 +589,18 @@
 				validityTF.setText("" + certificateConfig.duration);
 				JPanel validityPanel = Utils.newFieldPanel(Translation.get("gb.validity"),
 						validityTF, Translation.get("gb.duration.days").replace("{0}",  "").trim());
-				
+
 				JPanel p1 = new JPanel(new GridLayout(0, 1, 5, 2));
 				p1.add(siteNamePanel);
 				p1.add(validityPanel);
-				
+
 				DefaultOidsPanel oids = new DefaultOidsPanel(metadata);
 
 				JPanel panel = new JPanel(new BorderLayout());
 				panel.add(p1, BorderLayout.NORTH);
 				panel.add(oids, BorderLayout.CENTER);
 
-				int result = JOptionPane.showConfirmDialog(GitblitAuthority.this, 
+				int result = JOptionPane.showConfirmDialog(GitblitAuthority.this,
 						panel, Translation.get("gb.newCertificateDefaults"), JOptionPane.OK_CANCEL_OPTION,
 						JOptionPane.QUESTION_MESSAGE, new ImageIcon(getClass().getResource("/settings_32x32.png")));
 				if (result == JOptionPane.OK_OPTION) {
@@ -614,7 +609,7 @@
 						certificateConfig.duration = Integer.parseInt(validityTF.getText());
 						certificateConfig.store(config, metadata);
 						config.save();
-						
+
 						Map<String, String> updates = new HashMap<String, String>();
 						updates.put(Keys.web.siteName, siteNameTF.getText());
 						gitblitSettings.saveSettings(updates);
@@ -624,10 +619,10 @@
 				}
 			}
 		});
-		
+
 		newSSLCertificate = new JButton(new ImageIcon(getClass().getResource("/rosette_16x16.png")));
 		newSSLCertificate.setFocusable(false);
-		newSSLCertificate.setToolTipText(Translation.get("gb.newSSLCertificate"));		
+		newSSLCertificate.setToolTipText(Translation.get("gb.newSSLCertificate"));
 		newSSLCertificate.addActionListener(new ActionListener() {
 			@Override
 			public void actionPerformed(ActionEvent e) {
@@ -641,7 +636,7 @@
 				final Date expires = dialog.getExpiration();
 				final String hostname = dialog.getHostname();
 				final boolean serveCertificate = dialog.isServeCertificate();
-				
+
 				AuthorityWorker worker = new AuthorityWorker(GitblitAuthority.this) {
 
 					@Override
@@ -649,12 +644,12 @@
 						if (!prepareX509Infrastructure()) {
 							return false;
 						}
-						
+
 						// read CA private key and certificate
 						File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE);
 						PrivateKey caPrivateKey = X509Utils.getPrivateKey(X509Utils.CA_ALIAS, caKeystoreFile, caKeystorePassword);
 						X509Certificate caCert = X509Utils.getCertificate(X509Utils.CA_ALIAS, caKeystoreFile, caKeystorePassword);
-						
+
 						// generate new SSL certificate
 						X509Metadata metadata = new X509Metadata(hostname, caKeystorePassword);
 						setMetadataDefaults(metadata);
@@ -674,24 +669,24 @@
 					@Override
 					protected void onSuccess() {
 						if (serveCertificate) {
-							JOptionPane.showMessageDialog(GitblitAuthority.this, 
+							JOptionPane.showMessageDialog(GitblitAuthority.this,
 									MessageFormat.format(Translation.get("gb.sslCertificateGeneratedRestart"), hostname),
 									Translation.get("gb.newSSLCertificate"), JOptionPane.INFORMATION_MESSAGE);
 						} else {
-							JOptionPane.showMessageDialog(GitblitAuthority.this, 
+							JOptionPane.showMessageDialog(GitblitAuthority.this,
 								MessageFormat.format(Translation.get("gb.sslCertificateGenerated"), hostname),
 								Translation.get("gb.newSSLCertificate"), JOptionPane.INFORMATION_MESSAGE);
 						}
 					}
 				};
-				
+
 				worker.execute();
 			}
 		});
-		
+
 		JButton emailBundle = new JButton(new ImageIcon(getClass().getResource("/mail_16x16.png")));
 		emailBundle.setFocusable(false);
-		emailBundle.setToolTipText(Translation.get("gb.emailCertificateBundle"));		
+		emailBundle.setToolTipText(Translation.get("gb.emailCertificateBundle"));
 		emailBundle.addActionListener(new ActionListener() {
 			@Override
 			public void actionPerformed(ActionEvent e) {
@@ -708,7 +703,7 @@
 				if (!zip.exists()) {
 					return;
 				}
-				
+
 				AuthorityWorker worker = new AuthorityWorker(GitblitAuthority.this) {
 					@Override
 					protected Boolean doRequest() throws IOException {
@@ -726,15 +721,15 @@
 						JOptionPane.showMessageDialog(GitblitAuthority.this, MessageFormat.format(Translation.get("gb.clientCertificateBundleSent"),
 								ucm.user.getDisplayName()));
 					}
-					
+
 				};
-				worker.execute();				
+				worker.execute();
 			}
 		});
-		
+
 		JButton logButton = new JButton(new ImageIcon(getClass().getResource("/script_16x16.png")));
 		logButton.setFocusable(false);
-		logButton.setToolTipText(Translation.get("gb.log"));		
+		logButton.setToolTipText(Translation.get("gb.log"));
 		logButton.addActionListener(new ActionListener() {
 			@Override
 			public void actionPerformed(ActionEvent e) {
@@ -748,19 +743,21 @@
 				}
 			}
 		});
-		
+
 		final JTextField filterTextfield = new JTextField(15);
 		filterTextfield.addActionListener(new ActionListener() {
+			@Override
 			public void actionPerformed(ActionEvent e) {
 				filterUsers(filterTextfield.getText());
 			}
 		});
 		filterTextfield.addKeyListener(new KeyAdapter() {
+			@Override
 			public void keyReleased(KeyEvent e) {
 				filterUsers(filterTextfield.getText());
 			}
 		});
-		
+
 		JToolBar buttonControls = new JToolBar(JToolBar.HORIZONTAL);
 		buttonControls.setFloatable(false);
 		buttonControls.add(certificateDefaultsButton);
@@ -771,17 +768,17 @@
 		JPanel userControls = new JPanel(new FlowLayout(FlowLayout.RIGHT, Utils.MARGIN, Utils.MARGIN));
 		userControls.add(new JLabel(Translation.get("gb.filter")));
 		userControls.add(filterTextfield);
-		
+
 		JPanel topPanel = new JPanel(new BorderLayout(0, 0));
 		topPanel.add(buttonControls, BorderLayout.WEST);
 		topPanel.add(userControls, BorderLayout.EAST);
-		
+
 		JPanel leftPanel = new JPanel(new BorderLayout());
 		leftPanel.add(topPanel, BorderLayout.NORTH);
 		leftPanel.add(usersPanel, BorderLayout.CENTER);
-		
+
 		userCertificatePanel.setMinimumSize(new Dimension(375, 10));
-		
+
 		JLabel statusLabel = new JLabel();
 		statusLabel.setHorizontalAlignment(SwingConstants.RIGHT);
 		if (X509Utils.unlimitedStrength) {
@@ -789,9 +786,10 @@
 		} else {
 			statusLabel.setText("JCE Standard Encryption Policy");
 		}
-		
+
 		JPanel root = new JPanel(new BorderLayout()) {
 			private static final long serialVersionUID = 1L;
+			@Override
 			public Insets getInsets() {
 				return Utils.INSETS;
 			}
@@ -802,13 +800,16 @@
 		root.add(statusLabel, BorderLayout.SOUTH);
 		return root;
 	}
-	
+
 	private void filterUsers(final String fragment) {
+		table.clearSelection();
+		userCertificatePanel.setUserCertificateModel(null);
 		if (StringUtils.isEmpty(fragment)) {
 			table.setRowSorter(defaultSorter);
 			return;
 		}
 		RowFilter<UserCertificateTableModel, Object> containsFilter = new RowFilter<UserCertificateTableModel, Object>() {
+			@Override
 			public boolean include(Entry<? extends UserCertificateTableModel, ? extends Object> entry) {
 				for (int i = entry.getValueCount() - 1; i >= 0; i--) {
 					if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) {
@@ -823,7 +824,7 @@
 		sorter.setRowFilter(containsFilter);
 		table.setRowSorter(sorter);
 	}
-	
+
 	@Override
 	public void log(String message) {
 		BufferedWriter writer = null;
@@ -843,32 +844,22 @@
 			}
 		}
 	}
-	
+
 	private boolean sendEmail(UserModel user, X509Metadata metadata, File zip) {
 		// send email
 		try {
 			if (mail.isReady()) {
-				Message message = mail.createMessage(user.emailAddress);
-				message.setSubject("Your Gitblit client certificate for " + metadata.serverHostname);
-
-				// body of email
+				Mailing mailing = Mailing.newPlain();
+				mailing.subject = "Your Gitblit client certificate for " + metadata.serverHostname;
+				mailing.setRecipients(user.emailAddress);
 				String body = X509Utils.processTemplate(new File(folder, X509Utils.CERTS + File.separator + "mail.tmpl"), metadata);
 				if (StringUtils.isEmpty(body)) {
 					body = MessageFormat.format("Hi {0}\n\nHere is your client certificate bundle.\nInside the zip file are installation instructions.", user.getDisplayName());
 				}
-				Multipart mp = new MimeMultipart();
-				MimeBodyPart messagePart = new MimeBodyPart();
-				messagePart.setText(body);
-				mp.addBodyPart(messagePart);
+				mailing.content = body;
+				mailing.addAttachment(zip);
 
-				// attach zip
-				MimeBodyPart filePart = new MimeBodyPart();
-				FileDataSource fds = new FileDataSource(zip);
-				filePart.setDataHandler(new DataHandler(fds));
-				filePart.setFileName(fds.getName());
-				mp.addBodyPart(filePart);
-
-				message.setContent(mp);
+				Message message = mail.createMessage(mailing);
 
 				mail.sendNow(message);
 				return true;
@@ -880,13 +871,13 @@
 		}
 		return false;
 	}
-	
+
 	private void setMetadataDefaults(X509Metadata metadata) {
 		metadata.serverHostname = gitblitSettings.getString(Keys.web.siteName, Constants.NAME);
 		if (StringUtils.isEmpty(metadata.serverHostname)) {
 			metadata.serverHostname = Constants.NAME;
 		}
-		
+
 		// set default values from config file
 		File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
 		FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());
@@ -900,7 +891,7 @@
 			certificateConfig.update(metadata);
 		}
 	}
-	
+
 	private void updateAuthorityConfig(UserCertificateModel ucm) {
 		File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
 		FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());

--
Gitblit v1.9.1